Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 495eb8f3

History | View | Annotate | Download (48.6 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

    
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime
43

    
44
from django_tables2 import RequestConfig
45

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

    
72
import astakos.im.messages as astakos_messages
73

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

    
111
logger = logging.getLogger(__name__)
112

    
113
callpoint = AstakosCallpoint()
114

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

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

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

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

    
150

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

    
165

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

    
181

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

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

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

    
209

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

    
213

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

220
    **Arguments**
221

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

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

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

233
    **Template:**
234

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

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

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

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

    
253

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

    
266

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

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

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

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

283
    **Arguments**
284

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

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

292
    **Template:**
293

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

296
    **Settings:**
297

298
    The view expectes the following settings are defined:
299

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

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

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

    
344

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

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

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

359
    **Arguments**
360

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

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

368
    **Template:**
369

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

372
    **Settings:**
373

374
    The view expectes the following settings are defined:
375

376
    * LOGIN_URL: login uri
377
    """
378
    extra_context = extra_context or {}
379
    form = ProfileForm(
380
        instance=request.user,
381
        session_key=request.session.session_key
382
    )
383
    extra_context['next'] = request.GET.get('next')
384
    if request.method == 'POST':
385
        form = ProfileForm(
386
            request.POST,
387
            instance=request.user,
388
            session_key=request.session.session_key
389
        )
390
        if form.is_valid():
391
            try:
392
                prev_token = request.user.auth_token
393
                user = form.save(request=request)
394
                next = restrict_next(
395
                    request.POST.get('next'),
396
                    domain=COOKIE_DOMAIN
397
                )
398
                msg = _(astakos_messages.PROFILE_UPDATED)
399
                messages.success(request, msg)
400

    
401
                if form.email_changed:
402
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
403
                    messages.success(request, msg)
404
                if form.password_changed:
405
                    msg = _(astakos_messages.PASSWORD_CHANGED)
406
                    messages.success(request, msg)
407

    
408
                if next:
409
                    return redirect(next)
410
                else:
411
                    return redirect(reverse('edit_profile'))
412
            except ValueError, ve:
413
                messages.success(request, ve)
414
    elif request.method == "GET":
415
        request.user.is_verified = True
416
        request.user.save()
417

    
418
    # existing providers
419
    user_providers = request.user.get_active_auth_providers()
420

    
421
    # providers that user can add
422
    user_available_providers = request.user.get_available_auth_providers()
423

    
424
    extra_context['services'] = get_services_dict()
425
    return render_response(template_name,
426
                           profile_form = form,
427
                           user_providers = user_providers,
428
                           user_available_providers = user_available_providers,
429
                           context_instance = get_context(request,
430
                                                          extra_context))
431

    
432

    
433
@transaction.commit_manually
434
@require_http_methods(["GET", "POST"])
435
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
436
    """
437
    Allows a user to create a local account.
438

439
    In case of GET request renders a form for entering the user information.
440
    In case of POST handles the signup.
441

442
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
443
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
444
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
445
    (see activation_backends);
446

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

450
    On unsuccessful creation, renders ``template_name`` with an error message.
451

452
    **Arguments**
453

454
    ``template_name``
455
        A custom template to render. This is optional;
456
        if not specified, this will default to ``im/signup.html``.
457

458
    ``extra_context``
459
        An dictionary of variables to add to the template context.
460

461
    ``on_success``
462
        Resolvable view name to redirect on registration success.
463

464
    **Template:**
465

466
    im/signup.html or ``template_name`` keyword argument.
467
    """
468
    extra_context = extra_context or {}
469
    if request.user.is_authenticated():
470
        return HttpResponseRedirect(reverse('edit_profile'))
471

    
472
    provider = get_query(request).get('provider', 'local')
473
    if not auth_providers.get_provider(provider).is_available_for_create():
474
        raise PermissionDenied
475

    
476
    id = get_query(request).get('id')
477
    try:
478
        instance = AstakosUser.objects.get(id=id) if id else None
479
    except AstakosUser.DoesNotExist:
480
        instance = None
481

    
482
    third_party_token = request.REQUEST.get('third_party_token', None)
483
    if third_party_token:
484
        pending = get_object_or_404(PendingThirdPartyUser,
485
                                    token=third_party_token)
486
        provider = pending.provider
487
        instance = pending.get_user_instance()
488

    
489
    try:
490
        if not backend:
491
            backend = get_backend(request)
492
        form = backend.get_signup_form(provider, instance)
493
    except Exception, e:
494
        form = SimpleBackend(request).get_signup_form(provider)
495
        messages.error(request, e)
496
    if request.method == 'POST':
497
        if form.is_valid():
498
            user = form.save(commit=False)
499

    
500
            # delete previously unverified accounts
501
            if AstakosUser.objects.user_exists(user.email):
502
                AstakosUser.objects.get_by_identifier(user.email).delete()
503

    
504
            try:
505
                result = backend.handle_activation(user)
506
                status = messages.SUCCESS
507
                message = result.message
508

    
509
                form.store_user(user, request)
510

    
511
                if 'additional_email' in form.cleaned_data:
512
                    additional_email = form.cleaned_data['additional_email']
513
                    if additional_email != user.email:
514
                        user.additionalmail_set.create(email=additional_email)
515
                        msg = 'Additional email: %s saved for user %s.' % (
516
                            additional_email,
517
                            user.email
518
                        )
519
                        logger._log(LOGGING_LEVEL, msg, [])
520

    
521
                if user and user.is_active:
522
                    next = request.POST.get('next', '')
523
                    response = prepare_response(request, user, next=next)
524
                    transaction.commit()
525
                    return response
526

    
527
                transaction.commit()
528
                messages.add_message(request, status, message)
529
                return HttpResponseRedirect(reverse(on_success))
530

    
531
            except SendMailError, e:
532
                logger.exception(e)
533
                status = messages.ERROR
534
                message = e.message
535
                messages.error(request, message)
536
                transaction.rollback()
537
            except BaseException, e:
538
                logger.exception(e)
539
                message = _(astakos_messages.GENERIC_ERROR)
540
                messages.error(request, message)
541
                logger.exception(e)
542
                transaction.rollback()
543

    
544
    return render_response(template_name,
545
                           signup_form=form,
546
                           third_party_token=third_party_token,
547
                           provider=provider,
548
                           context_instance=get_context(request, extra_context))
549

    
550

    
551
@require_http_methods(["GET", "POST"])
552
@required_auth_methods_assigned(only_warn=True)
553
@login_required
554
@signed_terms_required
555
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
556
    """
557
    Allows a user to send feedback.
558

559
    In case of GET request renders a form for providing the feedback information.
560
    In case of POST sends an email to support team.
561

562
    If the user isn't logged in, redirects to settings.LOGIN_URL.
563

564
    **Arguments**
565

566
    ``template_name``
567
        A custom template to use. This is optional; if not specified,
568
        this will default to ``im/feedback.html``.
569

570
    ``extra_context``
571
        An dictionary of variables to add to the template context.
572

573
    **Template:**
574

575
    im/signup.html or ``template_name`` keyword argument.
576

577
    **Settings:**
578

579
    * LOGIN_URL: login uri
580
    """
581
    extra_context = extra_context or {}
582
    if request.method == 'GET':
583
        form = FeedbackForm()
584
    if request.method == 'POST':
585
        if not request.user:
586
            return HttpResponse('Unauthorized', status=401)
587

    
588
        form = FeedbackForm(request.POST)
589
        if form.is_valid():
590
            msg = form.cleaned_data['feedback_msg']
591
            data = form.cleaned_data['feedback_data']
592
            try:
593
                send_feedback(msg, data, request.user, email_template_name)
594
            except SendMailError, e:
595
                messages.error(request, message)
596
            else:
597
                message = _(astakos_messages.FEEDBACK_SENT)
598
                messages.success(request, message)
599
    return render_response(template_name,
600
                           feedback_form=form,
601
                           context_instance=get_context(request, extra_context))
602

    
603

    
604
@require_http_methods(["GET"])
605
@signed_terms_required
606
def logout(request, template='registration/logged_out.html', extra_context=None):
607
    """
608
    Wraps `django.contrib.auth.logout`.
609
    """
610
    extra_context = extra_context or {}
611
    response = HttpResponse()
612
    if request.user.is_authenticated():
613
        email = request.user.email
614
        auth_logout(request)
615
    else:
616
        response['Location'] = reverse('index')
617
        response.status_code = 301
618
        return response
619

    
620
    next = restrict_next(
621
        request.GET.get('next'),
622
        domain=COOKIE_DOMAIN
623
    )
624

    
625
    if next:
626
        response['Location'] = next
627
        response.status_code = 302
628
    elif LOGOUT_NEXT:
629
        response['Location'] = LOGOUT_NEXT
630
        response.status_code = 301
631
    else:
632
        message = _(astakos_messages.LOGOUT_SUCCESS)
633
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
634
        if last_provider:
635
            provider = auth_providers.get_provider(last_provider)
636
            extra_message = provider.get_logout_message_display
637
            if extra_message:
638
                message += '<br />' + extra_message
639
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
640
        response['Location'] = reverse('index')
641
        response.status_code = 301
642
    return response
643

    
644

    
645
@require_http_methods(["GET", "POST"])
646
@transaction.commit_manually
647
def activate(request, greeting_email_template_name='im/welcome_email.txt',
648
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
649
    """
650
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
651
    and renews the user token.
652

653
    The view uses commit_manually decorator in order to ensure the user state will be updated
654
    only if the email will be send successfully.
655
    """
656
    token = request.GET.get('auth')
657
    next = request.GET.get('next')
658
    try:
659
        user = AstakosUser.objects.get(auth_token=token)
660
    except AstakosUser.DoesNotExist:
661
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
662

    
663
    if user.is_active:
664
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
665
        messages.error(request, message)
666
        return index(request)
667

    
668
    try:
669
        activate_func(user, greeting_email_template_name,
670
                      helpdesk_email_template_name, verify_email=True)
671
        next = ACTIVATION_REDIRECT_URL or next
672
        response = prepare_response(request, user, next, renew=True)
673
        transaction.commit()
674
        return response
675
    except SendMailError, e:
676
        message = e.message
677
        messages.add_message(request, messages.ERROR, message)
678
        transaction.rollback()
679
        return index(request)
680
    except BaseException, e:
681
        status = messages.ERROR
682
        message = _(astakos_messages.GENERIC_ERROR)
683
        messages.add_message(request, messages.ERROR, message)
684
        logger.exception(e)
685
        transaction.rollback()
686
        return index(request)
687

    
688

    
689
@require_http_methods(["GET", "POST"])
690
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
691
    extra_context = extra_context or {}
692
    term = None
693
    terms = None
694
    if not term_id:
695
        try:
696
            term = ApprovalTerms.objects.order_by('-id')[0]
697
        except IndexError:
698
            pass
699
    else:
700
        try:
701
            term = ApprovalTerms.objects.get(id=term_id)
702
        except ApprovalTerms.DoesNotExist, e:
703
            pass
704

    
705
    if not term:
706
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
707
        return HttpResponseRedirect(reverse('index'))
708
    f = open(term.location, 'r')
709
    terms = f.read()
710

    
711
    if request.method == 'POST':
712
        next = restrict_next(
713
            request.POST.get('next'),
714
            domain=COOKIE_DOMAIN
715
        )
716
        if not next:
717
            next = reverse('index')
718
        form = SignApprovalTermsForm(request.POST, instance=request.user)
719
        if not form.is_valid():
720
            return render_response(template_name,
721
                                   terms=terms,
722
                                   approval_terms_form=form,
723
                                   context_instance=get_context(request, extra_context))
724
        user = form.save()
725
        return HttpResponseRedirect(next)
726
    else:
727
        form = None
728
        if request.user.is_authenticated() and not request.user.signed_terms:
729
            form = SignApprovalTermsForm(instance=request.user)
730
        return render_response(template_name,
731
                               terms=terms,
732
                               approval_terms_form=form,
733
                               context_instance=get_context(request, extra_context))
734

    
735

    
736
@require_http_methods(["GET", "POST"])
737
@transaction.commit_manually
738
def change_email(request, activation_key=None,
739
                 email_template_name='registration/email_change_email.txt',
740
                 form_template_name='registration/email_change_form.html',
741
                 confirm_template_name='registration/email_change_done.html',
742
                 extra_context=None):
743
    extra_context = extra_context or {}
744

    
745

    
746
    if not astakos_settings.EMAILCHANGE_ENABLED:
747
        raise PermissionDenied
748

    
749
    if activation_key:
750
        try:
751
            user = EmailChange.objects.change_email(activation_key)
752
            if request.user.is_authenticated() and request.user == user or not \
753
                    request.user.is_authenticated():
754
                msg = _(astakos_messages.EMAIL_CHANGED)
755
                messages.success(request, msg)
756
                transaction.commit()
757
                return HttpResponseRedirect(reverse('edit_profile'))
758
        except ValueError, e:
759
            messages.error(request, e)
760
            transaction.rollback()
761
            return HttpResponseRedirect(reverse('index'))
762

    
763
        return render_response(confirm_template_name,
764
                               modified_user=user if 'user' in locals() \
765
                               else None, context_instance=get_context(request,
766
                                                            extra_context))
767

    
768
    if not request.user.is_authenticated():
769
        path = quote(request.get_full_path())
770
        url = request.build_absolute_uri(reverse('index'))
771
        return HttpResponseRedirect(url + '?next=' + path)
772

    
773
    # clean up expired email changes
774
    if request.user.email_change_is_pending():
775
        change = request.user.emailchanges.get()
776
        if change.activation_key_expired():
777
            change.delete()
778
            transaction.commit()
779
            return HttpResponseRedirect(reverse('email_change'))
780

    
781
    form = EmailChangeForm(request.POST or None)
782
    if request.method == 'POST' and form.is_valid():
783
        try:
784
            ec = form.save(email_template_name, request)
785
        except SendMailError, e:
786
            msg = e
787
            messages.error(request, msg)
788
            transaction.rollback()
789
            return HttpResponseRedirect(reverse('edit_profile'))
790
        else:
791
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
792
            messages.success(request, msg)
793
            transaction.commit()
794
            return HttpResponseRedirect(reverse('edit_profile'))
795

    
796
    if request.user.email_change_is_pending():
797
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
798

    
799
    return render_response(
800
        form_template_name,
801
        form=form,
802
        context_instance=get_context(request, extra_context)
803
    )
804

    
805

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

    
808
    if request.user.is_authenticated():
809
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
810
        return HttpResponseRedirect(reverse('edit_profile'))
811

    
812
    if astakos_settings.MODERATION_ENABLED:
813
        raise PermissionDenied
814

    
815
    extra_context = extra_context or {}
816
    try:
817
        u = AstakosUser.objects.get(id=user_id)
818
    except AstakosUser.DoesNotExist:
819
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
820
    else:
821
        try:
822
            send_activation_func(u)
823
            msg = _(astakos_messages.ACTIVATION_SENT)
824
            messages.success(request, msg)
825
        except SendMailError, e:
826
            messages.error(request, e)
827
    return render_response(
828
        template_name,
829
        login_form = LoginForm(request=request),
830
        context_instance = get_context(
831
            request,
832
            extra_context
833
        )
834
    )
835

    
836

    
837
@require_http_methods(["GET"])
838
@valid_astakos_user_required
839
def resource_usage(request):
840

    
841
    def with_class(entry):
842
         entry['load_class'] = 'red'
843
         max_value = float(entry['maxValue'])
844
         curr_value = float(entry['currValue'])
845
         entry['ratio_limited']= 0
846
         if max_value > 0 :
847
             entry['ratio'] = (curr_value / max_value) * 100
848
         else:
849
             entry['ratio'] = 0
850
         if entry['ratio'] < 66:
851
             entry['load_class'] = 'yellow'
852
         if entry['ratio'] < 33:
853
             entry['load_class'] = 'green'
854
         if entry['ratio']<0:
855
             entry['ratio'] = 0
856
         if entry['ratio']>100:
857
             entry['ratio_limited'] = 100
858
         else:
859
             entry['ratio_limited'] = entry['ratio']
860
         return entry
861

    
862
    def pluralize(entry):
863
        entry['plural'] = engine.plural(entry.get('name'))
864
        return entry
865

    
866
    resource_usage = None
867
    result = callpoint.get_user_usage(request.user.id)
868
    if result.is_success:
869
        resource_usage = result.data
870
        backenddata = map(with_class, result.data)
871
        backenddata = map(pluralize , backenddata)
872
    else:
873
        messages.error(request, result.reason)
874
        backenddata = []
875
        resource_usage = []
876

    
877
    if request.REQUEST.get('json', None):
878
        return HttpResponse(json.dumps(backenddata),
879
                            mimetype="application/json")
880

    
881
    return render_response('im/resource_usage.html',
882
                           context_instance=get_context(request),
883
                           resource_usage=backenddata,
884
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
885
                           result=result)
886

    
887
# TODO: action only on POST and user should confirm the removal
888
@require_http_methods(["GET", "POST"])
889
@login_required
890
@signed_terms_required
891
def remove_auth_provider(request, pk):
892
    try:
893
        provider = request.user.auth_providers.get(pk=pk)
894
    except AstakosUserAuthProvider.DoesNotExist:
895
        raise Http404
896

    
897
    if provider.can_remove():
898
        provider.delete()
899
        return HttpResponseRedirect(reverse('edit_profile'))
900
    else:
901
        raise PermissionDenied
902

    
903

    
904
def how_it_works(request):
905
    return render_response(
906
        'im/how_it_works.html',
907
        context_instance=get_context(request))
908

    
909
@transaction.commit_manually
910
def _create_object(request, model=None, template_name=None,
911
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
912
        login_required=False, context_processors=None, form_class=None,
913
        msg=None):
914
    """
915
    Based of django.views.generic.create_update.create_object which displays a
916
    summary page before creating the object.
917
    """
918
    rollback = False
919
    response = None
920

    
921
    if extra_context is None: extra_context = {}
922
    if login_required and not request.user.is_authenticated():
923
        return redirect_to_login(request.path)
924
    try:
925

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

    
957
        if response == None:
958
            # Create the template, context, response
959
            if not template_name:
960
                template_name = "%s/%s_form.html" %\
961
                     (model._meta.app_label, model._meta.object_name.lower())
962
            t = template_loader.get_template(template_name)
963
            c = RequestContext(request, {
964
                'form': form
965
            }, context_processors)
966
            apply_extra_context(extra_context, c)
967
            response = HttpResponse(t.render(c))
968
        return response
969

    
970
@transaction.commit_manually
971
def _update_object(request, model=None, object_id=None, slug=None,
972
        slug_field='slug', template_name=None, template_loader=template_loader,
973
        extra_context=None, post_save_redirect=None, login_required=False,
974
        context_processors=None, template_object_name='object',
975
        form_class=None, msg=None):
976
    """
977
    Based of django.views.generic.create_update.update_object which displays a
978
    summary page before updating the object.
979
    """
980
    rollback = False
981
    response = None
982

    
983
    if extra_context is None: extra_context = {}
984
    if login_required and not request.user.is_authenticated():
985
        return redirect_to_login(request.path)
986

    
987
    try:
988
        model, form_class = get_model_and_form_class(model, form_class)
989
        obj = lookup_object(model, object_id, slug, slug_field)
990

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

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

    
1068

    
1069
@require_http_methods(["GET"])
1070
@signed_terms_required
1071
@login_required
1072
def project_list(request):
1073
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1074
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1075
                                                prefix="my_projects_")
1076
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1077

    
1078
    return object_list(
1079
        request,
1080
        projects,
1081
        template_name='im/projects/project_list.html',
1082
        extra_context={
1083
            'is_search':False,
1084
            'table': table,
1085
        })
1086

    
1087

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

    
1124

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

    
1157
    rollback = False
1158

    
1159
    application = get_object_or_404(ProjectApplication, pk=application_id)
1160
    try:
1161
        members = application.project.projectmembership_set.select_related()
1162
    except Project.DoesNotExist:
1163
        members = ProjectMembership.objects.none()
1164

    
1165
    members_table = tables.ProjectApplicationMembersTable(application,
1166
                                                          members,
1167
                                                          user=request.user,
1168
                                                          prefix="members_")
1169
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1170

    
1171
    modifications_table = None
1172
    if application.follower:
1173
        following_applications = list(application.followers())
1174
        following_applications.reverse()
1175
        modifications_table = \
1176
            tables.ProjectModificationApplicationsTable(following_applications,
1177
                                                       user=request.user,
1178
                                                       prefix="modifications_")
1179

    
1180
    return object_detail(
1181
        request,
1182
        queryset=ProjectApplication.objects.select_related(),
1183
        object_id=application_id,
1184
        template_name='im/projects/project_detail.html',
1185
        extra_context={
1186
            'addmembers_form':addmembers_form,
1187
            'members_table': members_table,
1188
            'user_owns_project': request.user.owns_project(application),
1189
            'modifications_table': modifications_table,
1190
            'member_status': application.user_status(request.user)
1191
            })
1192

    
1193
@require_http_methods(["GET", "POST"])
1194
@signed_terms_required
1195
@login_required
1196
def project_search(request):
1197
    q = request.GET.get('q', '')
1198
    form = ProjectSearchForm()
1199
    q = q.strip()
1200

    
1201
    if request.method == "POST":
1202
        form = ProjectSearchForm(request.POST)
1203
        if form.is_valid():
1204
            q = form.cleaned_data['q'].strip()
1205
        else:
1206
            q = None
1207

    
1208
    if q is None:
1209
        projects = ProjectApplication.objects.none()
1210
    else:
1211
        accepted_projects = request.user.projectmembership_set.filter(
1212
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1213
        projects = ProjectApplication.objects.search_by_name(q)
1214
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1215
        projects = projects.exclude(project__in=accepted_projects)
1216

    
1217
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1218
                                                prefix="my_projects_")
1219
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1220

    
1221
    return object_list(
1222
        request,
1223
        projects,
1224
        template_name='im/projects/project_list.html',
1225
        extra_context={
1226
          'form': form,
1227
          'is_search': True,
1228
          'q': q,
1229
          'table': table
1230
        })
1231

    
1232
@require_http_methods(["POST", "GET"])
1233
@signed_terms_required
1234
@login_required
1235
@transaction.commit_manually
1236
def project_join(request, application_id):
1237
    next = request.GET.get('next')
1238
    if not next:
1239
        next = reverse('astakos.im.views.project_detail',
1240
                       args=(application_id,))
1241

    
1242
    rollback = False
1243
    try:
1244
        application_id = int(application_id)
1245
        join_project(application_id, request.user)
1246
        # TODO: distinct messages for request/auto accept ???
1247
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1248
    except (IOError, PermissionDenied), e:
1249
        messages.error(request, e)
1250
    except BaseException, e:
1251
        logger.exception(e)
1252
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1253
        rollback = True
1254
    finally:
1255
        if rollback:
1256
            transaction.rollback()
1257
        else:
1258
            transaction.commit()
1259
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1260
    return redirect(next)
1261

    
1262
@require_http_methods(["POST"])
1263
@signed_terms_required
1264
@login_required
1265
@transaction.commit_manually
1266
def project_leave(request, application_id):
1267
    next = request.GET.get('next')
1268
    if not next:
1269
        next = reverse('astakos.im.views.project_list')
1270

    
1271
    rollback = False
1272
    try:
1273
        application_id = int(application_id)
1274
        leave_project(application_id, request.user)
1275
    except (IOError, PermissionDenied), e:
1276
        messages.error(request, e)
1277
    except BaseException, e:
1278
        logger.exception(e)
1279
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1280
        rollback = True
1281
    finally:
1282
        if rollback:
1283
            transaction.rollback()
1284
        else:
1285
            transaction.commit()
1286

    
1287
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1288
    return redirect(next)
1289

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

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

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

    
1371