Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (48.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.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
    get_by_chain_or_404)
101
from astakos.im.settings import (
102
    COOKIE_DOMAIN, LOGOUT_NEXT,
103
    LOGGING_LEVEL, PAGINATE_BY,
104
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
105
    ACTIVATION_REDIRECT_URL,
106
    MODERATION_ENABLED)
107
from astakos.im.api import get_services_dict
108
from astakos.im import settings as astakos_settings
109
from astakos.im.api.callpoint import AstakosCallpoint
110
from astakos.im import auth_providers
111

    
112
logger = logging.getLogger(__name__)
113

    
114
callpoint = AstakosCallpoint()
115

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

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

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

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

    
151

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

    
166

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

    
182

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

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

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

    
210

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

    
214

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

221
    **Arguments**
222

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

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

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

234
    **Template:**
235

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

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

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

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

    
254

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

    
267

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

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

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

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

284
    **Arguments**
285

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

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

293
    **Template:**
294

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

297
    **Settings:**
298

299
    The view expectes the following settings are defined:
300

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

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

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

    
345

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

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

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

360
    **Arguments**
361

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

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

369
    **Template:**
370

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

373
    **Settings:**
374

375
    The view expectes the following settings are defined:
376

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

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

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

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

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

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

    
433

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

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

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

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

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

453
    **Arguments**
454

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

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

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

465
    **Template:**
466

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

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

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

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

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

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

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

    
510
                form.store_user(user, request)
511

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

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

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

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

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

    
551

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

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

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

565
    **Arguments**
566

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

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

574
    **Template:**
575

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

578
    **Settings:**
579

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

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

    
604

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

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

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

    
645

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

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

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

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

    
689

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

    
706
    if not term:
707
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
708
        return HttpResponseRedirect(reverse('index'))
709
    try:
710
        f = open(term.location, 'r')
711
    except IOError:
712
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
713
        return render_response(
714
            template_name, context_instance=get_context(request, extra_context))
715

    
716
    terms = f.read()
717

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

    
742

    
743
@require_http_methods(["GET", "POST"])
744
@transaction.commit_manually
745
def change_email(request, activation_key=None,
746
                 email_template_name='registration/email_change_email.txt',
747
                 form_template_name='registration/email_change_form.html',
748
                 confirm_template_name='registration/email_change_done.html',
749
                 extra_context=None):
750
    extra_context = extra_context or {}
751

    
752

    
753
    if not astakos_settings.EMAILCHANGE_ENABLED:
754
        raise PermissionDenied
755

    
756
    if activation_key:
757
        try:
758
            user = EmailChange.objects.change_email(activation_key)
759
            if request.user.is_authenticated() and request.user == user or not \
760
                    request.user.is_authenticated():
761
                msg = _(astakos_messages.EMAIL_CHANGED)
762
                messages.success(request, msg)
763
                transaction.commit()
764
                return HttpResponseRedirect(reverse('edit_profile'))
765
        except ValueError, e:
766
            messages.error(request, e)
767
            transaction.rollback()
768
            return HttpResponseRedirect(reverse('index'))
769

    
770
        return render_response(confirm_template_name,
771
                               modified_user=user if 'user' in locals() \
772
                               else None, context_instance=get_context(request,
773
                                                            extra_context))
774

    
775
    if not request.user.is_authenticated():
776
        path = quote(request.get_full_path())
777
        url = request.build_absolute_uri(reverse('index'))
778
        return HttpResponseRedirect(url + '?next=' + path)
779

    
780
    # clean up expired email changes
781
    if request.user.email_change_is_pending():
782
        change = request.user.emailchanges.get()
783
        if change.activation_key_expired():
784
            change.delete()
785
            transaction.commit()
786
            return HttpResponseRedirect(reverse('email_change'))
787

    
788
    form = EmailChangeForm(request.POST or None)
789
    if request.method == 'POST' and form.is_valid():
790
        try:
791
            ec = form.save(email_template_name, request)
792
        except SendMailError, e:
793
            msg = e
794
            messages.error(request, msg)
795
            transaction.rollback()
796
            return HttpResponseRedirect(reverse('edit_profile'))
797
        else:
798
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
799
            messages.success(request, msg)
800
            transaction.commit()
801
            return HttpResponseRedirect(reverse('edit_profile'))
802

    
803
    if request.user.email_change_is_pending():
804
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
805

    
806
    return render_response(
807
        form_template_name,
808
        form=form,
809
        context_instance=get_context(request, extra_context)
810
    )
811

    
812

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

    
815
    if request.user.is_authenticated():
816
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
817
        return HttpResponseRedirect(reverse('edit_profile'))
818

    
819
    if astakos_settings.MODERATION_ENABLED:
820
        raise PermissionDenied
821

    
822
    extra_context = extra_context or {}
823
    try:
824
        u = AstakosUser.objects.get(id=user_id)
825
    except AstakosUser.DoesNotExist:
826
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
827
    else:
828
        try:
829
            send_activation_func(u)
830
            msg = _(astakos_messages.ACTIVATION_SENT)
831
            messages.success(request, msg)
832
        except SendMailError, e:
833
            messages.error(request, e)
834
    return render_response(
835
        template_name,
836
        login_form = LoginForm(request=request),
837
        context_instance = get_context(
838
            request,
839
            extra_context
840
        )
841
    )
842

    
843

    
844
@require_http_methods(["GET"])
845
@valid_astakos_user_required
846
def resource_usage(request):
847

    
848
    def with_class(entry):
849
         entry['load_class'] = 'red'
850
         max_value = float(entry['maxValue'])
851
         curr_value = float(entry['currValue'])
852
         entry['ratio_limited']= 0
853
         if max_value > 0 :
854
             entry['ratio'] = (curr_value / max_value) * 100
855
         else:
856
             entry['ratio'] = 0
857
         if entry['ratio'] < 66:
858
             entry['load_class'] = 'yellow'
859
         if entry['ratio'] < 33:
860
             entry['load_class'] = 'green'
861
         if entry['ratio']<0:
862
             entry['ratio'] = 0
863
         if entry['ratio']>100:
864
             entry['ratio_limited'] = 100
865
         else:
866
             entry['ratio_limited'] = entry['ratio']
867
         return entry
868

    
869
    def pluralize(entry):
870
        entry['plural'] = engine.plural(entry.get('name'))
871
        return entry
872

    
873
    resource_usage = None
874
    result = callpoint.get_user_usage(request.user.id)
875
    if result.is_success:
876
        resource_usage = result.data
877
        backenddata = map(with_class, result.data)
878
        backenddata = map(pluralize , backenddata)
879
    else:
880
        messages.error(request, result.reason)
881
        backenddata = []
882
        resource_usage = []
883

    
884
    if request.REQUEST.get('json', None):
885
        return HttpResponse(json.dumps(backenddata),
886
                            mimetype="application/json")
887

    
888
    return render_response('im/resource_usage.html',
889
                           context_instance=get_context(request),
890
                           resource_usage=backenddata,
891
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
892
                           result=result)
893

    
894
# TODO: action only on POST and user should confirm the removal
895
@require_http_methods(["GET", "POST"])
896
@login_required
897
@signed_terms_required
898
def remove_auth_provider(request, pk):
899
    try:
900
        provider = request.user.auth_providers.get(pk=pk)
901
    except AstakosUserAuthProvider.DoesNotExist:
902
        raise Http404
903

    
904
    if provider.can_remove():
905
        provider.delete()
906
        return HttpResponseRedirect(reverse('edit_profile'))
907
    else:
908
        raise PermissionDenied
909

    
910

    
911
def how_it_works(request):
912
    return render_response(
913
        'im/how_it_works.html',
914
        context_instance=get_context(request))
915

    
916
@transaction.commit_manually
917
def _create_object(request, model=None, template_name=None,
918
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
919
        login_required=False, context_processors=None, form_class=None,
920
        msg=None):
921
    """
922
    Based of django.views.generic.create_update.create_object which displays a
923
    summary page before creating the object.
924
    """
925
    rollback = False
926
    response = None
927

    
928
    if extra_context is None: extra_context = {}
929
    if login_required and not request.user.is_authenticated():
930
        return redirect_to_login(request.path)
931
    try:
932

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

    
964
        if response == None:
965
            # Create the template, context, response
966
            if not template_name:
967
                template_name = "%s/%s_form.html" %\
968
                     (model._meta.app_label, model._meta.object_name.lower())
969
            t = template_loader.get_template(template_name)
970
            c = RequestContext(request, {
971
                'form': form
972
            }, context_processors)
973
            apply_extra_context(extra_context, c)
974
            response = HttpResponse(t.render(c))
975
        return response
976

    
977
@transaction.commit_manually
978
def _update_object(request, model=None, object_id=None, slug=None,
979
        slug_field='slug', template_name=None, template_loader=template_loader,
980
        extra_context=None, post_save_redirect=None, login_required=False,
981
        context_processors=None, template_object_name='object',
982
        form_class=None, msg=None):
983
    """
984
    Based of django.views.generic.create_update.update_object which displays a
985
    summary page before updating the object.
986
    """
987
    rollback = False
988
    response = None
989

    
990
    if extra_context is None: extra_context = {}
991
    if login_required and not request.user.is_authenticated():
992
        return redirect_to_login(request.path)
993

    
994
    try:
995
        model, form_class = get_model_and_form_class(model, form_class)
996
        obj = lookup_object(model, object_id, slug, slug_field)
997

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

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

    
1075

    
1076
@require_http_methods(["GET"])
1077
@signed_terms_required
1078
@login_required
1079
def project_list(request):
1080
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1081
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1082
                                                prefix="my_projects_")
1083
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1084

    
1085
    return object_list(
1086
        request,
1087
        projects,
1088
        template_name='im/projects/project_list.html',
1089
        extra_context={
1090
            'is_search':False,
1091
            'table': table,
1092
        })
1093

    
1094

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

    
1131

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

    
1164
    rollback = False
1165

    
1166
    project, application = get_by_chain_or_404(chain_id)
1167
    if project:
1168
        members = project.projectmembership_set.select_related()
1169
    else:
1170
        members = ProjectMembership.objects.none()
1171

    
1172
    members_table = tables.ProjectApplicationMembersTable(application,
1173
                                                          members,
1174
                                                          user=request.user,
1175
                                                          prefix="members_")
1176
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1177

    
1178
    modifications_table = None
1179
    if application.follower:
1180
        following_applications = list(application.followers())
1181
        following_applications.reverse()
1182
        modifications_table = \
1183
            tables.ProjectModificationApplicationsTable(following_applications,
1184
                                                       user=request.user,
1185
                                                       prefix="modifications_")
1186

    
1187
    return object_detail(
1188
        request,
1189
        queryset=ProjectApplication.objects.select_related(),
1190
        object_id=application.id,
1191
        template_name='im/projects/project_detail.html',
1192
        extra_context={
1193
            'addmembers_form':addmembers_form,
1194
            'members_table': members_table,
1195
            'user_owns_project': request.user.owns_project(application),
1196
            'modifications_table': modifications_table,
1197
            'member_status': application.user_status(request.user)
1198
            })
1199

    
1200
@require_http_methods(["GET", "POST"])
1201
@signed_terms_required
1202
@login_required
1203
def project_search(request):
1204
    q = request.GET.get('q', '')
1205
    form = ProjectSearchForm()
1206
    q = q.strip()
1207

    
1208
    if request.method == "POST":
1209
        form = ProjectSearchForm(request.POST)
1210
        if form.is_valid():
1211
            q = form.cleaned_data['q'].strip()
1212
        else:
1213
            q = None
1214

    
1215
    if q is None:
1216
        projects = ProjectApplication.objects.none()
1217
    else:
1218
        accepted_projects = request.user.projectmembership_set.filter(
1219
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1220
        projects = ProjectApplication.objects.search_by_name(q)
1221
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1222
        projects = projects.exclude(project__in=accepted_projects)
1223

    
1224
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1225
                                                prefix="my_projects_")
1226
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1227

    
1228
    return object_list(
1229
        request,
1230
        projects,
1231
        template_name='im/projects/project_list.html',
1232
        extra_context={
1233
          'form': form,
1234
          'is_search': True,
1235
          'q': q,
1236
          'table': table
1237
        })
1238

    
1239
@require_http_methods(["POST", "GET"])
1240
@signed_terms_required
1241
@login_required
1242
@transaction.commit_manually
1243
def project_join(request, chain_id):
1244
    next = request.GET.get('next')
1245
    if not next:
1246
        next = reverse('astakos.im.views.project_detail',
1247
                       args=(chain_id,))
1248

    
1249
    rollback = False
1250
    try:
1251
        chain_id = int(chain_id)
1252
        join_project(chain_id, request.user)
1253
        # TODO: distinct messages for request/auto accept ???
1254
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1255
    except (IOError, PermissionDenied), e:
1256
        messages.error(request, e)
1257
    except BaseException, e:
1258
        logger.exception(e)
1259
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1260
        rollback = True
1261
    finally:
1262
        if rollback:
1263
            transaction.rollback()
1264
        else:
1265
            transaction.commit()
1266
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1267
    return redirect(next)
1268

    
1269
@require_http_methods(["POST"])
1270
@signed_terms_required
1271
@login_required
1272
@transaction.commit_manually
1273
def project_leave(request, chain_id):
1274
    next = request.GET.get('next')
1275
    if not next:
1276
        next = reverse('astakos.im.views.project_list')
1277

    
1278
    rollback = False
1279
    try:
1280
        chain_id = int(chain_id)
1281
        leave_project(chain_id, request.user)
1282
    except (IOError, PermissionDenied), e:
1283
        messages.error(request, e)
1284
    except BaseException, e:
1285
        logger.exception(e)
1286
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1287
        rollback = True
1288
    finally:
1289
        if rollback:
1290
            transaction.rollback()
1291
        else:
1292
            transaction.commit()
1293

    
1294
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1295
    return redirect(next)
1296

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

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

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

    
1378
def landing(request):
1379
    return render_response(
1380
        'im/landing.html',
1381
        context_instance=get_context(request))