Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 05617ab9

History | View | Annotate | Download (47.9 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, ProfileForm,
85
    FeedbackForm, SignApprovalTermsForm,
86
    EmailChangeForm,
87
    ProjectApplicationForm, ProjectSortForm,
88
    AddProjectMembersForm, ProjectSearchForm,
89
    ProjectMembersSortForm)
90
from astakos.im.functions import (
91
    send_feedback, SendMailError,
92
    logout as auth_logout,
93
    activate as activate_func,
94
    invite,
95
    send_activation as send_activation_func,
96
    SendNotificationError,
97
    accept_membership, reject_membership, remove_membership,
98
    leave_project, join_project, enroll_member)
99
from astakos.im.settings import (
100
    COOKIE_DOMAIN, LOGOUT_NEXT,
101
    LOGGING_LEVEL, PAGINATE_BY,
102
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
103
    MODERATION_ENABLED)
104
from astakos.im.api import get_services_dict
105
from astakos.im import settings as astakos_settings
106
from astakos.im.api.callpoint import AstakosCallpoint
107
from astakos.im import auth_providers
108

    
109
logger = logging.getLogger(__name__)
110

    
111
callpoint = AstakosCallpoint()
112

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

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

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

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

    
148

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

    
163

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

    
179

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

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

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

    
207

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

    
211

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

218
    **Arguments**
219

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

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

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

231
    **Template:**
232

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

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

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

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

    
251

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

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

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

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

268
    **Arguments**
269

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

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

277
    **Template:**
278

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

281
    **Settings:**
282

283
    The view expectes the following settings are defined:
284

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

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

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

    
329

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

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

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

344
    **Arguments**
345

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

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

353
    **Template:**
354

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

357
    **Settings:**
358

359
    The view expectes the following settings are defined:
360

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

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

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

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

    
411

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

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

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

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

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

431
    **Arguments**
432

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

437
    ``extra_context``
438
        An dictionary of variables to add to the template context.
439

440
    ``on_success``
441
        Resolvable view name to redirect on registration success.
442

443
    **Template:**
444

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

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

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

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

    
468
    try:
469
        if not backend:
470
            backend = get_backend(request)
471
        form = backend.get_signup_form(provider, instance)
472
    except Exception, e:
473
        form = SimpleBackend(request).get_signup_form(provider)
474
        messages.error(request, e)
475
    if request.method == 'POST':
476
        if form.is_valid():
477
            user = form.save(commit=False)
478

    
479
            # delete previously unverified accounts
480
            if AstakosUser.objects.user_exists(user.email):
481
                AstakosUser.objects.get_by_identifier(user.email).delete()
482

    
483
            try:
484
                result = backend.handle_activation(user)
485
                status = messages.SUCCESS
486
                message = result.message
487

    
488
                form.store_user(user, request)
489

    
490
                if 'additional_email' in form.cleaned_data:
491
                    additional_email = form.cleaned_data['additional_email']
492
                    if additional_email != user.email:
493
                        user.additionalmail_set.create(email=additional_email)
494
                        msg = 'Additional email: %s saved for user %s.' % (
495
                            additional_email,
496
                            user.email
497
                        )
498
                        logger._log(LOGGING_LEVEL, msg, [])
499

    
500
                if user and user.is_active:
501
                    next = request.POST.get('next', '')
502
                    response = prepare_response(request, user, next=next)
503
                    transaction.commit()
504
                    return response
505

    
506
                transaction.commit()
507
                messages.add_message(request, status, message)
508
                return HttpResponseRedirect(reverse(on_success))
509

    
510
            except SendMailError, e:
511
                logger.exception(e)
512
                status = messages.ERROR
513
                message = e.message
514
                messages.error(request, message)
515
                transaction.rollback()
516
            except BaseException, e:
517
                logger.exception(e)
518
                message = _(astakos_messages.GENERIC_ERROR)
519
                messages.error(request, message)
520
                logger.exception(e)
521
                transaction.rollback()
522

    
523
    return render_response(template_name,
524
                           signup_form=form,
525
                           third_party_token=third_party_token,
526
                           provider=provider,
527
                           context_instance=get_context(request, extra_context))
528

    
529

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

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

541
    If the user isn't logged in, redirects to settings.LOGIN_URL.
542

543
    **Arguments**
544

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

549
    ``extra_context``
550
        An dictionary of variables to add to the template context.
551

552
    **Template:**
553

554
    im/signup.html or ``template_name`` keyword argument.
555

556
    **Settings:**
557

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

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

    
582

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

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

    
604
    if next:
605
        response['Location'] = next
606
        response.status_code = 302
607
    elif LOGOUT_NEXT:
608
        response['Location'] = LOGOUT_NEXT
609
        response.status_code = 301
610
    else:
611
        message = _(astakos_messages.LOGOUT_SUCCESS)
612
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
613
        if last_provider:
614
            provider = auth_providers.get_provider(last_provider)
615
            extra_message = provider.get_logout_message_display
616
            if extra_message:
617
                message += '<br />' + extra_message
618
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
619
        response['Location'] = reverse('index')
620
        response.status_code = 301
621
    return response
622

    
623

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

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

    
642
    if user.is_active:
643
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
644
        messages.error(request, message)
645
        return index(request)
646

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

    
665

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

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

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

    
712

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

    
723

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

    
739
        return render_response(confirm_template_name,
740
                               modified_user=user if 'user' in locals() \
741
                               else None, context_instance=get_context(request,
742
                                                            extra_context))
743

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

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

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

    
774
    if request.user.email_change_is_pending():
775
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
776

    
777
    return render_response(
778
        form_template_name,
779
        form=form,
780
        context_instance=get_context(request, extra_context)
781
    )
782

    
783

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

    
786
    if request.user.is_authenticated():
787
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
788
        return HttpResponseRedirect(reverse('edit_profile'))
789

    
790
    if astakos_settings.MODERATION_ENABLED:
791
        raise PermissionDenied
792

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

    
814

    
815
@require_http_methods(["GET"])
816
@valid_astakos_user_required
817
def resource_usage(request):
818

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

    
840
    def pluralize(entry):
841
        entry['plural'] = engine.plural(entry.get('name'))
842
        return entry
843

    
844
    resource_usage = None
845
    result = callpoint.get_user_usage(request.user.id)
846
    if result.is_success:
847
        resource_usage = result.data
848
        backenddata = map(with_class, result.data)
849
        backenddata = map(pluralize , backenddata)
850
    else:
851
        messages.error(request, result.reason)
852
        backenddata = []
853
        resource_usage = []
854

    
855
    if request.REQUEST.get('json', None):
856
        return HttpResponse(json.dumps(backenddata),
857
                            mimetype="application/json")
858

    
859
    return render_response('im/resource_usage.html',
860
                           context_instance=get_context(request),
861
                           resource_usage=backenddata,
862
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
863
                           result=result)
864

    
865
# TODO: action only on POST and user should confirm the removal
866
@require_http_methods(["GET", "POST"])
867
@login_required
868
@signed_terms_required
869
def remove_auth_provider(request, pk):
870
    try:
871
        provider = request.user.auth_providers.get(pk=pk)
872
    except AstakosUserAuthProvider.DoesNotExist:
873
        raise Http404
874

    
875
    if provider.can_remove():
876
        provider.delete()
877
        return HttpResponseRedirect(reverse('edit_profile'))
878
    else:
879
        raise PermissionDenied
880

    
881

    
882
def how_it_works(request):
883
    return render_response(
884
        'im/how_it_works.html',
885
        context_instance=get_context(request))
886

    
887
@transaction.commit_manually
888
def _create_object(request, model=None, template_name=None,
889
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
890
        login_required=False, context_processors=None, form_class=None,
891
        msg=None):
892
    """
893
    Based of django.views.generic.create_update.create_object which displays a
894
    summary page before creating the object.
895
    """
896
    rollback = False
897
    response = None
898

    
899
    if extra_context is None: extra_context = {}
900
    if login_required and not request.user.is_authenticated():
901
        return redirect_to_login(request.path)
902
    try:
903

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

    
935
        if response == None:
936
            # Create the template, context, response
937
            if not template_name:
938
                template_name = "%s/%s_form.html" %\
939
                     (model._meta.app_label, model._meta.object_name.lower())
940
            t = template_loader.get_template(template_name)
941
            c = RequestContext(request, {
942
                'form': form
943
            }, context_processors)
944
            apply_extra_context(extra_context, c)
945
            response = HttpResponse(t.render(c))
946
        return response
947

    
948
@transaction.commit_manually
949
def _update_object(request, model=None, object_id=None, slug=None,
950
        slug_field='slug', template_name=None, template_loader=template_loader,
951
        extra_context=None, post_save_redirect=None, login_required=False,
952
        context_processors=None, template_object_name='object',
953
        form_class=None, msg=None):
954
    """
955
    Based of django.views.generic.create_update.update_object which displays a
956
    summary page before updating the object.
957
    """
958
    rollback = False
959
    response = None
960

    
961
    if extra_context is None: extra_context = {}
962
    if login_required and not request.user.is_authenticated():
963
        return redirect_to_login(request.path)
964

    
965
    try:
966
        model, form_class = get_model_and_form_class(model, form_class)
967
        obj = lookup_object(model, object_id, slug, slug_field)
968

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

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

    
1046

    
1047
@require_http_methods(["GET"])
1048
@signed_terms_required
1049
@login_required
1050
def project_list(request):
1051
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1052
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1053
                                                prefix="my_projects_")
1054
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1055

    
1056
    return object_list(
1057
        request,
1058
        projects,
1059
        template_name='im/projects/project_list.html',
1060
        extra_context={
1061
            'is_search':False,
1062
            'table': table,
1063
        })
1064

    
1065

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

    
1102

    
1103
@require_http_methods(["GET", "POST"])
1104
@signed_terms_required
1105
@login_required
1106
@transaction.commit_on_success
1107
def project_detail(request, application_id):
1108
    addmembers_form = AddProjectMembersForm()
1109
    if request.method == 'POST':
1110
        addmembers_form = AddProjectMembersForm(
1111
            request.POST,
1112
            application_id=int(application_id),
1113
            request_user=request.user)
1114
        if addmembers_form.is_valid():
1115
            try:
1116
                rollback = False
1117
                application_id = int(application_id)
1118
                map(lambda u: enroll_member(
1119
                        application_id,
1120
                        u,
1121
                        request_user=request.user),
1122
                    addmembers_form.valid_users)
1123
            except (IOError, PermissionDenied), e:
1124
                messages.error(request, e)
1125
            except BaseException, e:
1126
                rollback = True
1127
                messages.error(request, e)
1128
            finally:
1129
                if rollback == True:
1130
                    transaction.rollback()
1131
                else:
1132
                    transaction.commit()
1133
            addmembers_form = AddProjectMembersForm()
1134

    
1135
    rollback = False
1136

    
1137
    application = get_object_or_404(ProjectApplication, pk=application_id)
1138
    try:
1139
        members = application.project.projectmembership_set.select_related()
1140
    except Project.DoesNotExist:
1141
        members = ProjectMembership.objects.none()
1142

    
1143
    members_table = tables.ProjectApplicationMembersTable(application,
1144
                                                          members,
1145
                                                          user=request.user,
1146
                                                          prefix="members_")
1147
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1148

    
1149
    modifications_table = None
1150
    if application.follower:
1151
        following_applications = list(application.followers())
1152
        following_applications.reverse()
1153
        modifications_table = \
1154
            tables.ProjectModificationApplicationsTable(following_applications,
1155
                                                       user=request.user,
1156
                                                       prefix="modifications_")
1157

    
1158
    return object_detail(
1159
        request,
1160
        queryset=ProjectApplication.objects.select_related(),
1161
        object_id=application_id,
1162
        template_name='im/projects/project_detail.html',
1163
        extra_context={
1164
            'addmembers_form':addmembers_form,
1165
            'members_table': members_table,
1166
            'user_owns_project': request.user.owns_project(application),
1167
            'modifications_table': modifications_table,
1168
            'member_status': application.user_status(request.user)
1169
            })
1170

    
1171
@require_http_methods(["GET", "POST"])
1172
@signed_terms_required
1173
@login_required
1174
def project_search(request):
1175
    q = request.GET.get('q', '')
1176
    form = ProjectSearchForm()
1177
    q = q.strip()
1178

    
1179
    if request.method == "POST":
1180
        form = ProjectSearchForm(request.POST)
1181
        if form.is_valid():
1182
            q = form.cleaned_data['q'].strip()
1183
        else:
1184
            q = None
1185

    
1186
    if q is None:
1187
        projects = ProjectApplication.objects.none()
1188
    else:
1189
        accepted_projects = request.user.projectmembership_set.filter(
1190
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1191
        projects = ProjectApplication.objects.search_by_name(q)
1192
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1193
        projects = projects.exclude(project__in=accepted_projects)
1194

    
1195
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1196
                                                prefix="my_projects_")
1197
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1198

    
1199
    return object_list(
1200
        request,
1201
        projects,
1202
        template_name='im/projects/project_list.html',
1203
        extra_context={
1204
          'form': form,
1205
          'is_search': True,
1206
          'q': q,
1207
          'table': table
1208
        })
1209

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

    
1220
    rollback = False
1221
    try:
1222
        application_id = int(application_id)
1223
        join_project(application_id, request.user)
1224
        # TODO: distinct messages for request/auto accept ???
1225
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1226
    except (IOError, PermissionDenied), e:
1227
        messages.error(request, e)
1228
    except BaseException, e:
1229
        logger.exception(e)
1230
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1231
        rollback = True
1232
    finally:
1233
        if rollback:
1234
            transaction.rollback()
1235
        else:
1236
            transaction.commit()
1237
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1238
    return redirect(next)
1239

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

    
1249
    rollback = False
1250
    try:
1251
        application_id = int(application_id)
1252
        leave_project(application_id, request.user)
1253
    except (IOError, PermissionDenied), e:
1254
        messages.error(request, e)
1255
    except BaseException, e:
1256
        logger.exception(e)
1257
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1258
        rollback = True
1259
    finally:
1260
        if rollback:
1261
            transaction.rollback()
1262
        else:
1263
            transaction.commit()
1264

    
1265
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1266
    return redirect(next)
1267

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

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

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

    
1349