Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 17ad5d37

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

    
110
logger = logging.getLogger(__name__)
111

    
112
callpoint = AstakosCallpoint()
113

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

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

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

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

    
149

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

    
164

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

    
180

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

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

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

    
208

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

    
212

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

219
    **Arguments**
220

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

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

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

232
    **Template:**
233

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

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

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

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

    
252

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

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

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

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

269
    **Arguments**
270

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

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

278
    **Template:**
279

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

282
    **Settings:**
283

284
    The view expectes the following settings are defined:
285

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

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

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

    
330

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

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

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

345
    **Arguments**
346

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

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

354
    **Template:**
355

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

358
    **Settings:**
359

360
    The view expectes the following settings are defined:
361

362
    * LOGIN_URL: login uri
363
    """
364
    extra_context = extra_context or {}
365
    form = ProfileForm(
366
        instance=request.user,
367
        session_key=request.session.session_key
368
    )
369
    extra_context['next'] = request.GET.get('next')
370
    if request.method == 'POST':
371
        form = ProfileForm(
372
            request.POST,
373
            instance=request.user,
374
            session_key=request.session.session_key
375
        )
376
        if form.is_valid():
377
            try:
378
                prev_token = request.user.auth_token
379
                user = form.save(request=request)
380
                next = restrict_next(
381
                    request.POST.get('next'),
382
                    domain=COOKIE_DOMAIN
383
                )
384
                msg = _(astakos_messages.PROFILE_UPDATED)
385
                messages.success(request, msg)
386
                if next:
387
                    return redirect(next)
388
                else:
389
                    return redirect(reverse('edit_profile'))
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='index', 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
    ``extra_context``
437
        An dictionary of variables to add to the template context.
438

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

442
    **Template:**
443

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

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

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

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

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

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

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

    
487
                form.store_user(user, request)
488

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

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

    
505
                transaction.commit()
506
                messages.add_message(request, status, message)
507
                return HttpResponseRedirect(reverse(on_success))
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

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

    
528

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

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

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

542
    **Arguments**
543

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

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

551
    **Template:**
552

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

555
    **Settings:**
556

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

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

    
581

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

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

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

    
622

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

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

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

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

    
664

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

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

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

    
711

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

    
721

    
722
    if not astakos_settings.EMAILCHANGE_ENABLED:
723
        raise PermissionDenied
724

    
725
    if activation_key:
726
        try:
727
            user = EmailChange.objects.change_email(activation_key)
728
            if request.user.is_authenticated() and request.user == user or not \
729
                    request.user.is_authenticated():
730
                msg = _(astakos_messages.EMAIL_CHANGED)
731
                messages.success(request, msg)
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
            ec = form.save(email_template_name, request)
761
        except SendMailError, e:
762
            msg = e
763
            messages.error(request, msg)
764
            transaction.rollback()
765
            return HttpResponseRedirect(reverse('edit_profile'))
766
        else:
767
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
768
            messages.success(request, msg)
769
            transaction.commit()
770
            return HttpResponseRedirect(reverse('edit_profile'))
771

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

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

    
781

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

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

    
788
    if astakos_settings.MODERATION_ENABLED:
789
        raise PermissionDenied
790

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

    
812

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

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

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

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

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

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

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

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

    
879

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

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

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

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

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

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

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

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

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

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

    
1044

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

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

    
1063

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

    
1100

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

    
1133
    rollback = False
1134

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1263
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1264
    return redirect(next)
1265

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

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

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

    
1347