Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 3fbf7863

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
@valid_astakos_user_required
714
@transaction.commit_manually
715
def change_email(request, activation_key=None,
716
                 email_template_name='registration/email_change_email.txt',
717
                 form_template_name='registration/email_change_form.html',
718
                 confirm_template_name='registration/email_change_done.html',
719
                 extra_context=None):
720
    extra_context = extra_context or {}
721

    
722

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

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

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

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

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

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

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

    
782

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

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

    
789
    if astakos_settings.MODERATION_ENABLED:
790
        raise PermissionDenied
791

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

    
813

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

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

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

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

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

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

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

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

    
880

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

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

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

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

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

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

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

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

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

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

    
1045

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

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

    
1064

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

    
1101

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

    
1134
    rollback = False
1135

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1348