Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 9b32e2fb

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

    
71
import astakos.im.messages as astakos_messages
72

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

    
108
logger = logging.getLogger(__name__)
109

    
110
callpoint = AstakosCallpoint()
111

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

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

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

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

    
147

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

    
162

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

    
178

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

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

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

    
206

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

    
210

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

217
    **Arguments**
218

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

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

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

230
    **Template:**
231

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

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

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

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

    
250

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

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

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

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

267
    **Arguments**
268

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

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

276
    **Template:**
277

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

280
    **Settings:**
281

282
    The view expectes the following settings are defined:
283

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

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

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

    
328

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

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

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

343
    **Arguments**
344

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

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

352
    **Template:**
353

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

356
    **Settings:**
357

358
    The view expectes the following settings are defined:
359

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

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

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

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

    
410

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

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

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

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

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

430
    **Arguments**
431

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

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

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

443
    **Template:**
444

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

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

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

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

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

    
484
                form.store_user(user, request)
485

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

    
527

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

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

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

541
    **Arguments**
542

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

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

550
    **Template:**
551

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

554
    **Settings:**
555

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

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

    
580

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

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

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

    
614

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

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

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

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

    
656

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

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

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

    
703

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

    
714

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

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

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

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

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

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

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

    
774

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

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

    
781
    if astakos_settings.MODERATION_ENABLED:
782
        raise PermissionDenied
783

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

    
805

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

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

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

    
835
    resource_usage = None
836
    result = callpoint.get_user_usage(request.user.id)
837
    if result.is_success:
838
        resource_usage = result.data
839
        backenddata = map(with_class, result.data)
840
        backenddata = map(pluralize , backenddata)
841
    else:
842
        messages.error(request, result.reason)
843
        backenddata = []
844
        resource_usage = []
845

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

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

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

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

    
872

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

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

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

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

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

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

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

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

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

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

    
1037

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

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

    
1056

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

    
1092

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

    
1125
    rollback = False
1126

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

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

    
1139
    modifications_table = None
1140
    if application.follower:
1141
        following_applications = list(application.followers())
1142
        following_applications.reverse()
1143
        modifications_table = \
1144
            tables.ProjectModificationApplicationsTable(following_applications,
1145
                                                       user=request.user,
1146
                                                       prefix="modifications_")
1147

    
1148
    return object_detail(
1149
        request,
1150
        queryset=ProjectApplication.objects.select_related(),
1151
        object_id=application_id,
1152
        template_name='im/projects/project_detail.html',
1153
        extra_context={
1154
            'addmembers_form':addmembers_form,
1155
            'members_table': members_table,
1156
            'user_owns_project': request.user.owns_project(application),
1157
            'modifications_table': modifications_table,
1158
            'member_status': application.user_status(request.user)
1159
            })
1160

    
1161
@require_http_methods(["GET", "POST"])
1162
@signed_terms_required
1163
@login_required
1164
def project_search(request):
1165
    q = request.GET.get('q', '')
1166
    form = ProjectSearchForm()
1167
    q = q.strip()
1168

    
1169
    if request.method == "POST":
1170
        form = ProjectSearchForm(request.POST)
1171
        if form.is_valid():
1172
            q = form.cleaned_data['q'].strip()
1173
        else:
1174
            q = None
1175

    
1176
    if q is None:
1177
        projects = ProjectApplication.objects.none()
1178
    else:
1179
        accepted_projects = request.user.projectmembership_set.filter(
1180
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1181
        projects = ProjectApplication.objects.search_by_name(q)
1182
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1183
        projects = projects.exclude(project__in=accepted_projects)
1184

    
1185
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1186
                                                prefix="my_projects_")
1187
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1188

    
1189
    return object_list(
1190
        request,
1191
        projects,
1192
        template_name='im/projects/project_list.html',
1193
        extra_context={
1194
          'form': form,
1195
          'is_search': True,
1196
          'q': q,
1197
          'table': table
1198
        })
1199

    
1200
@require_http_methods(["POST", "GET"])
1201
@signed_terms_required
1202
@login_required
1203
@transaction.commit_manually
1204
def project_join(request, application_id):
1205
    next = request.GET.get('next')
1206
    if not next:
1207
        next = reverse('astakos.im.views.project_detail',
1208
                       args=(application_id,))
1209

    
1210
    rollback = False
1211
    try:
1212
        application_id = int(application_id)
1213
        join_project(application_id, request.user)
1214
        # TODO: distinct messages for request/auto accept ???
1215
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1216
    except (IOError, PermissionDenied), e:
1217
        messages.error(request, e)
1218
    except BaseException, e:
1219
        logger.exception(e)
1220
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1221
        rollback = True
1222
    finally:
1223
        if rollback:
1224
            transaction.rollback()
1225
        else:
1226
            transaction.commit()
1227
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1228
    return redirect(next)
1229

    
1230
@require_http_methods(["POST"])
1231
@signed_terms_required
1232
@login_required
1233
@transaction.commit_manually
1234
def project_leave(request, application_id):
1235
    next = request.GET.get('next')
1236
    if not next:
1237
        next = reverse('astakos.im.views.project_list')
1238

    
1239
    rollback = False
1240
    try:
1241
        application_id = int(application_id)
1242
        leave_project(application_id, request.user)
1243
    except (IOError, PermissionDenied), e:
1244
        messages.error(request, e)
1245
    except BaseException, e:
1246
        logger.exception(e)
1247
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1248
        rollback = True
1249
    finally:
1250
        if rollback:
1251
            transaction.rollback()
1252
        else:
1253
            transaction.commit()
1254

    
1255
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1256
    return redirect(next)
1257

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

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

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

    
1339