Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 0504f010

History | View | Annotate | Download (48.2 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(["POST"])
254
@valid_astakos_user_required
255
def update_token(request):
256
    """
257
    Update api token view.
258
    """
259
    user = request.user
260
    user.renew_token()
261
    user.save()
262
    messages.success(request, astakos_messages.TOKEN_UPDATED)
263
    return HttpResponseRedirect(reverse('edit_profile'))
264

    
265

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

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

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

280
    If the user isn't logged in, redirects to settings.LOGIN_URL.
281

282
    **Arguments**
283

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

288
    ``extra_context``
289
        An dictionary of variables to add to the template context.
290

291
    **Template:**
292

293
    im/invitations.html or ``template_name`` keyword argument.
294

295
    **Settings:**
296

297
    The view expectes the following settings are defined:
298

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

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

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

    
343

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

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

356
    If the user isn't logged in, redirects to settings.LOGIN_URL.
357

358
    **Arguments**
359

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

364
    ``extra_context``
365
        An dictionary of variables to add to the template context.
366

367
    **Template:**
368

369
    im/profile.html or ``template_name`` keyword argument.
370

371
    **Settings:**
372

373
    The view expectes the following settings are defined:
374

375
    * LOGIN_URL: login uri
376
    """
377
    extra_context = extra_context or {}
378
    form = ProfileForm(
379
        instance=request.user,
380
        session_key=request.session.session_key
381
    )
382
    extra_context['next'] = request.GET.get('next')
383
    if request.method == 'POST':
384
        form = ProfileForm(
385
            request.POST,
386
            instance=request.user,
387
            session_key=request.session.session_key
388
        )
389
        if form.is_valid():
390
            try:
391
                prev_token = request.user.auth_token
392
                user = form.save(request=request)
393
                next = restrict_next(
394
                    request.POST.get('next'),
395
                    domain=COOKIE_DOMAIN
396
                )
397
                msg = _(astakos_messages.PROFILE_UPDATED)
398
                messages.success(request, msg)
399
                if next:
400
                    return redirect(next)
401
                else:
402
                    return redirect(reverse('edit_profile'))
403
            except ValueError, ve:
404
                messages.success(request, ve)
405
    elif request.method == "GET":
406
        request.user.is_verified = True
407
        request.user.save()
408

    
409
    # existing providers
410
    user_providers = request.user.get_active_auth_providers()
411

    
412
    # providers that user can add
413
    user_available_providers = request.user.get_available_auth_providers()
414

    
415
    extra_context['services'] = get_services_dict()
416
    return render_response(template_name,
417
                           profile_form = form,
418
                           user_providers = user_providers,
419
                           user_available_providers = user_available_providers,
420
                           context_instance = get_context(request,
421
                                                          extra_context))
422

    
423

    
424
@transaction.commit_manually
425
@require_http_methods(["GET", "POST"])
426
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
427
    """
428
    Allows a user to create a local account.
429

430
    In case of GET request renders a form for entering the user information.
431
    In case of POST handles the signup.
432

433
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
434
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
435
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
436
    (see activation_backends);
437

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

441
    On unsuccessful creation, renders ``template_name`` with an error message.
442

443
    **Arguments**
444

445
    ``template_name``
446
        A custom template to render. This is optional;
447
        if not specified, this will default to ``im/signup.html``.
448

449
    ``extra_context``
450
        An dictionary of variables to add to the template context.
451

452
    ``on_success``
453
        Resolvable view name to redirect on registration success.
454

455
    **Template:**
456

457
    im/signup.html or ``template_name`` keyword argument.
458
    """
459
    extra_context = extra_context or {}
460
    if request.user.is_authenticated():
461
        return HttpResponseRedirect(reverse('edit_profile'))
462

    
463
    provider = get_query(request).get('provider', 'local')
464
    if not auth_providers.get_provider(provider).is_available_for_create():
465
        raise PermissionDenied
466

    
467
    id = get_query(request).get('id')
468
    try:
469
        instance = AstakosUser.objects.get(id=id) if id else None
470
    except AstakosUser.DoesNotExist:
471
        instance = None
472

    
473
    third_party_token = request.REQUEST.get('third_party_token', None)
474
    if third_party_token:
475
        pending = get_object_or_404(PendingThirdPartyUser,
476
                                    token=third_party_token)
477
        provider = pending.provider
478
        instance = pending.get_user_instance()
479

    
480
    try:
481
        if not backend:
482
            backend = get_backend(request)
483
        form = backend.get_signup_form(provider, instance)
484
    except Exception, e:
485
        form = SimpleBackend(request).get_signup_form(provider)
486
        messages.error(request, e)
487
    if request.method == 'POST':
488
        if form.is_valid():
489
            user = form.save(commit=False)
490

    
491
            # delete previously unverified accounts
492
            if AstakosUser.objects.user_exists(user.email):
493
                AstakosUser.objects.get_by_identifier(user.email).delete()
494

    
495
            try:
496
                result = backend.handle_activation(user)
497
                status = messages.SUCCESS
498
                message = result.message
499

    
500
                form.store_user(user, request)
501

    
502
                if 'additional_email' in form.cleaned_data:
503
                    additional_email = form.cleaned_data['additional_email']
504
                    if additional_email != user.email:
505
                        user.additionalmail_set.create(email=additional_email)
506
                        msg = 'Additional email: %s saved for user %s.' % (
507
                            additional_email,
508
                            user.email
509
                        )
510
                        logger._log(LOGGING_LEVEL, msg, [])
511

    
512
                if user and user.is_active:
513
                    next = request.POST.get('next', '')
514
                    response = prepare_response(request, user, next=next)
515
                    transaction.commit()
516
                    return response
517

    
518
                transaction.commit()
519
                messages.add_message(request, status, message)
520
                return HttpResponseRedirect(reverse(on_success))
521

    
522
            except SendMailError, e:
523
                logger.exception(e)
524
                status = messages.ERROR
525
                message = e.message
526
                messages.error(request, message)
527
                transaction.rollback()
528
            except BaseException, e:
529
                logger.exception(e)
530
                message = _(astakos_messages.GENERIC_ERROR)
531
                messages.error(request, message)
532
                logger.exception(e)
533
                transaction.rollback()
534

    
535
    return render_response(template_name,
536
                           signup_form=form,
537
                           third_party_token=third_party_token,
538
                           provider=provider,
539
                           context_instance=get_context(request, extra_context))
540

    
541

    
542
@require_http_methods(["GET", "POST"])
543
@required_auth_methods_assigned(only_warn=True)
544
@login_required
545
@signed_terms_required
546
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
547
    """
548
    Allows a user to send feedback.
549

550
    In case of GET request renders a form for providing the feedback information.
551
    In case of POST sends an email to support team.
552

553
    If the user isn't logged in, redirects to settings.LOGIN_URL.
554

555
    **Arguments**
556

557
    ``template_name``
558
        A custom template to use. This is optional; if not specified,
559
        this will default to ``im/feedback.html``.
560

561
    ``extra_context``
562
        An dictionary of variables to add to the template context.
563

564
    **Template:**
565

566
    im/signup.html or ``template_name`` keyword argument.
567

568
    **Settings:**
569

570
    * LOGIN_URL: login uri
571
    """
572
    extra_context = extra_context or {}
573
    if request.method == 'GET':
574
        form = FeedbackForm()
575
    if request.method == 'POST':
576
        if not request.user:
577
            return HttpResponse('Unauthorized', status=401)
578

    
579
        form = FeedbackForm(request.POST)
580
        if form.is_valid():
581
            msg = form.cleaned_data['feedback_msg']
582
            data = form.cleaned_data['feedback_data']
583
            try:
584
                send_feedback(msg, data, request.user, email_template_name)
585
            except SendMailError, e:
586
                messages.error(request, message)
587
            else:
588
                message = _(astakos_messages.FEEDBACK_SENT)
589
                messages.success(request, message)
590
    return render_response(template_name,
591
                           feedback_form=form,
592
                           context_instance=get_context(request, extra_context))
593

    
594

    
595
@require_http_methods(["GET"])
596
@signed_terms_required
597
def logout(request, template='registration/logged_out.html', extra_context=None):
598
    """
599
    Wraps `django.contrib.auth.logout`.
600
    """
601
    extra_context = extra_context or {}
602
    response = HttpResponse()
603
    if request.user.is_authenticated():
604
        email = request.user.email
605
        auth_logout(request)
606
    else:
607
        response['Location'] = reverse('index')
608
        response.status_code = 301
609
        return response
610

    
611
    next = restrict_next(
612
        request.GET.get('next'),
613
        domain=COOKIE_DOMAIN
614
    )
615

    
616
    if next:
617
        response['Location'] = next
618
        response.status_code = 302
619
    elif LOGOUT_NEXT:
620
        response['Location'] = LOGOUT_NEXT
621
        response.status_code = 301
622
    else:
623
        message = _(astakos_messages.LOGOUT_SUCCESS)
624
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
625
        if last_provider:
626
            provider = auth_providers.get_provider(last_provider)
627
            extra_message = provider.get_logout_message_display
628
            if extra_message:
629
                message += '<br />' + extra_message
630
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
631
        response['Location'] = reverse('index')
632
        response.status_code = 301
633
    return response
634

    
635

    
636
@require_http_methods(["GET", "POST"])
637
@transaction.commit_manually
638
def activate(request, greeting_email_template_name='im/welcome_email.txt',
639
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
640
    """
641
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
642
    and renews the user token.
643

644
    The view uses commit_manually decorator in order to ensure the user state will be updated
645
    only if the email will be send successfully.
646
    """
647
    token = request.GET.get('auth')
648
    next = request.GET.get('next')
649
    try:
650
        user = AstakosUser.objects.get(auth_token=token)
651
    except AstakosUser.DoesNotExist:
652
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
653

    
654
    if user.is_active:
655
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
656
        messages.error(request, message)
657
        return index(request)
658

    
659
    try:
660
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
661
        response = prepare_response(request, user, next, renew=True)
662
        transaction.commit()
663
        return response
664
    except SendMailError, e:
665
        message = e.message
666
        messages.add_message(request, messages.ERROR, message)
667
        transaction.rollback()
668
        return index(request)
669
    except BaseException, e:
670
        status = messages.ERROR
671
        message = _(astakos_messages.GENERIC_ERROR)
672
        messages.add_message(request, messages.ERROR, message)
673
        logger.exception(e)
674
        transaction.rollback()
675
        return index(request)
676

    
677

    
678
@require_http_methods(["GET", "POST"])
679
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
680
    extra_context = extra_context or {}
681
    term = None
682
    terms = None
683
    if not term_id:
684
        try:
685
            term = ApprovalTerms.objects.order_by('-id')[0]
686
        except IndexError:
687
            pass
688
    else:
689
        try:
690
            term = ApprovalTerms.objects.get(id=term_id)
691
        except ApprovalTerms.DoesNotExist, e:
692
            pass
693

    
694
    if not term:
695
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
696
        return HttpResponseRedirect(reverse('index'))
697
    f = open(term.location, 'r')
698
    terms = f.read()
699

    
700
    if request.method == 'POST':
701
        next = restrict_next(
702
            request.POST.get('next'),
703
            domain=COOKIE_DOMAIN
704
        )
705
        if not next:
706
            next = reverse('index')
707
        form = SignApprovalTermsForm(request.POST, instance=request.user)
708
        if not form.is_valid():
709
            return render_response(template_name,
710
                                   terms=terms,
711
                                   approval_terms_form=form,
712
                                   context_instance=get_context(request, extra_context))
713
        user = form.save()
714
        return HttpResponseRedirect(next)
715
    else:
716
        form = None
717
        if request.user.is_authenticated() and not request.user.signed_terms:
718
            form = SignApprovalTermsForm(instance=request.user)
719
        return render_response(template_name,
720
                               terms=terms,
721
                               approval_terms_form=form,
722
                               context_instance=get_context(request, extra_context))
723

    
724

    
725
@require_http_methods(["GET", "POST"])
726
@transaction.commit_manually
727
def change_email(request, activation_key=None,
728
                 email_template_name='registration/email_change_email.txt',
729
                 form_template_name='registration/email_change_form.html',
730
                 confirm_template_name='registration/email_change_done.html',
731
                 extra_context=None):
732
    extra_context = extra_context or {}
733

    
734

    
735
    if not astakos_settings.EMAILCHANGE_ENABLED:
736
        raise PermissionDenied
737

    
738
    if activation_key:
739
        try:
740
            user = EmailChange.objects.change_email(activation_key)
741
            if request.user.is_authenticated() and request.user == user or not \
742
                    request.user.is_authenticated():
743
                msg = _(astakos_messages.EMAIL_CHANGED)
744
                messages.success(request, msg)
745
                transaction.commit()
746
                return HttpResponseRedirect(reverse('edit_profile'))
747
        except ValueError, e:
748
            messages.error(request, e)
749
            transaction.rollback()
750
            return HttpResponseRedirect(reverse('index'))
751

    
752
        return render_response(confirm_template_name,
753
                               modified_user=user if 'user' in locals() \
754
                               else None, context_instance=get_context(request,
755
                                                            extra_context))
756

    
757
    if not request.user.is_authenticated():
758
        path = quote(request.get_full_path())
759
        url = request.build_absolute_uri(reverse('index'))
760
        return HttpResponseRedirect(url + '?next=' + path)
761

    
762
    # clean up expired email changes
763
    if request.user.email_change_is_pending():
764
        change = request.user.emailchanges.get()
765
        if change.activation_key_expired():
766
            change.delete()
767
            transaction.commit()
768
            return HttpResponseRedirect(reverse('email_change'))
769

    
770
    form = EmailChangeForm(request.POST or None)
771
    if request.method == 'POST' and form.is_valid():
772
        try:
773
            ec = form.save(email_template_name, request)
774
        except SendMailError, e:
775
            msg = e
776
            messages.error(request, msg)
777
            transaction.rollback()
778
            return HttpResponseRedirect(reverse('edit_profile'))
779
        else:
780
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
781
            messages.success(request, msg)
782
            transaction.commit()
783
            return HttpResponseRedirect(reverse('edit_profile'))
784

    
785
    if request.user.email_change_is_pending():
786
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
787

    
788
    return render_response(
789
        form_template_name,
790
        form=form,
791
        context_instance=get_context(request, extra_context)
792
    )
793

    
794

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

    
797
    if request.user.is_authenticated():
798
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
799
        return HttpResponseRedirect(reverse('edit_profile'))
800

    
801
    if astakos_settings.MODERATION_ENABLED:
802
        raise PermissionDenied
803

    
804
    extra_context = extra_context or {}
805
    try:
806
        u = AstakosUser.objects.get(id=user_id)
807
    except AstakosUser.DoesNotExist:
808
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
809
    else:
810
        try:
811
            send_activation_func(u)
812
            msg = _(astakos_messages.ACTIVATION_SENT)
813
            messages.success(request, msg)
814
        except SendMailError, e:
815
            messages.error(request, e)
816
    return render_response(
817
        template_name,
818
        login_form = LoginForm(request=request),
819
        context_instance = get_context(
820
            request,
821
            extra_context
822
        )
823
    )
824

    
825

    
826
@require_http_methods(["GET"])
827
@valid_astakos_user_required
828
def resource_usage(request):
829

    
830
    def with_class(entry):
831
         entry['load_class'] = 'red'
832
         max_value = float(entry['maxValue'])
833
         curr_value = float(entry['currValue'])
834
         entry['ratio_limited']= 0
835
         if max_value > 0 :
836
             entry['ratio'] = (curr_value / max_value) * 100
837
         else:
838
             entry['ratio'] = 0
839
         if entry['ratio'] < 66:
840
             entry['load_class'] = 'yellow'
841
         if entry['ratio'] < 33:
842
             entry['load_class'] = 'green'
843
         if entry['ratio']<0:
844
             entry['ratio'] = 0
845
         if entry['ratio']>100:
846
             entry['ratio_limited'] = 100
847
         else:
848
             entry['ratio_limited'] = entry['ratio']
849
         return entry
850

    
851
    def pluralize(entry):
852
        entry['plural'] = engine.plural(entry.get('name'))
853
        return entry
854

    
855
    resource_usage = None
856
    result = callpoint.get_user_usage(request.user.id)
857
    if result.is_success:
858
        resource_usage = result.data
859
        backenddata = map(with_class, result.data)
860
        backenddata = map(pluralize , backenddata)
861
    else:
862
        messages.error(request, result.reason)
863
        backenddata = []
864
        resource_usage = []
865

    
866
    if request.REQUEST.get('json', None):
867
        return HttpResponse(json.dumps(backenddata),
868
                            mimetype="application/json")
869

    
870
    return render_response('im/resource_usage.html',
871
                           context_instance=get_context(request),
872
                           resource_usage=backenddata,
873
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
874
                           result=result)
875

    
876
# TODO: action only on POST and user should confirm the removal
877
@require_http_methods(["GET", "POST"])
878
@login_required
879
@signed_terms_required
880
def remove_auth_provider(request, pk):
881
    try:
882
        provider = request.user.auth_providers.get(pk=pk)
883
    except AstakosUserAuthProvider.DoesNotExist:
884
        raise Http404
885

    
886
    if provider.can_remove():
887
        provider.delete()
888
        return HttpResponseRedirect(reverse('edit_profile'))
889
    else:
890
        raise PermissionDenied
891

    
892

    
893
def how_it_works(request):
894
    return render_response(
895
        'im/how_it_works.html',
896
        context_instance=get_context(request))
897

    
898
@transaction.commit_manually
899
def _create_object(request, model=None, template_name=None,
900
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
901
        login_required=False, context_processors=None, form_class=None,
902
        msg=None):
903
    """
904
    Based of django.views.generic.create_update.create_object which displays a
905
    summary page before creating the object.
906
    """
907
    rollback = False
908
    response = None
909

    
910
    if extra_context is None: extra_context = {}
911
    if login_required and not request.user.is_authenticated():
912
        return redirect_to_login(request.path)
913
    try:
914

    
915
        model, form_class = get_model_and_form_class(model, form_class)
916
        extra_context['edit'] = 0
917
        if request.method == 'POST':
918
            form = form_class(request.POST, request.FILES)
919
            if form.is_valid():
920
                verify = request.GET.get('verify')
921
                edit = request.GET.get('edit')
922
                if verify == '1':
923
                    extra_context['show_form'] = False
924
                    extra_context['form_data'] = form.cleaned_data
925
                elif edit == '1':
926
                    extra_context['show_form'] = True
927
                else:
928
                    new_object = form.save()
929
                    if not msg:
930
                        msg = _("The %(verbose_name)s was created successfully.")
931
                    msg = msg % model._meta.__dict__
932
                    messages.success(request, msg, fail_silently=True)
933
                    response = redirect(post_save_redirect, new_object)
934
        else:
935
            form = form_class()
936
    except BaseException, e:
937
        logger.exception(e)
938
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
939
        rollback = True
940
    finally:
941
        if rollback:
942
            transaction.rollback()
943
        else:
944
            transaction.commit()
945

    
946
        if response == None:
947
            # Create the template, context, response
948
            if not template_name:
949
                template_name = "%s/%s_form.html" %\
950
                     (model._meta.app_label, model._meta.object_name.lower())
951
            t = template_loader.get_template(template_name)
952
            c = RequestContext(request, {
953
                'form': form
954
            }, context_processors)
955
            apply_extra_context(extra_context, c)
956
            response = HttpResponse(t.render(c))
957
        return response
958

    
959
@transaction.commit_manually
960
def _update_object(request, model=None, object_id=None, slug=None,
961
        slug_field='slug', template_name=None, template_loader=template_loader,
962
        extra_context=None, post_save_redirect=None, login_required=False,
963
        context_processors=None, template_object_name='object',
964
        form_class=None, msg=None):
965
    """
966
    Based of django.views.generic.create_update.update_object which displays a
967
    summary page before updating the object.
968
    """
969
    rollback = False
970
    response = None
971

    
972
    if extra_context is None: extra_context = {}
973
    if login_required and not request.user.is_authenticated():
974
        return redirect_to_login(request.path)
975

    
976
    try:
977
        model, form_class = get_model_and_form_class(model, form_class)
978
        obj = lookup_object(model, object_id, slug, slug_field)
979

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

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

    
1057

    
1058
@require_http_methods(["GET"])
1059
@signed_terms_required
1060
@login_required
1061
def project_list(request):
1062
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1063
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1064
                                                prefix="my_projects_")
1065
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1066

    
1067
    return object_list(
1068
        request,
1069
        projects,
1070
        template_name='im/projects/project_list.html',
1071
        extra_context={
1072
            'is_search':False,
1073
            'table': table,
1074
        })
1075

    
1076

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

    
1113

    
1114
@require_http_methods(["GET", "POST"])
1115
@signed_terms_required
1116
@login_required
1117
@transaction.commit_on_success
1118
def project_detail(request, application_id):
1119
    addmembers_form = AddProjectMembersForm()
1120
    if request.method == 'POST':
1121
        addmembers_form = AddProjectMembersForm(
1122
            request.POST,
1123
            application_id=int(application_id),
1124
            request_user=request.user)
1125
        if addmembers_form.is_valid():
1126
            try:
1127
                rollback = False
1128
                application_id = int(application_id)
1129
                map(lambda u: enroll_member(
1130
                        application_id,
1131
                        u,
1132
                        request_user=request.user),
1133
                    addmembers_form.valid_users)
1134
            except (IOError, PermissionDenied), e:
1135
                messages.error(request, e)
1136
            except BaseException, e:
1137
                rollback = True
1138
                messages.error(request, e)
1139
            finally:
1140
                if rollback == True:
1141
                    transaction.rollback()
1142
                else:
1143
                    transaction.commit()
1144
            addmembers_form = AddProjectMembersForm()
1145

    
1146
    rollback = False
1147

    
1148
    application = get_object_or_404(ProjectApplication, pk=application_id)
1149
    try:
1150
        members = application.project.projectmembership_set.select_related()
1151
    except Project.DoesNotExist:
1152
        members = ProjectMembership.objects.none()
1153

    
1154
    members_table = tables.ProjectApplicationMembersTable(application,
1155
                                                          members,
1156
                                                          user=request.user,
1157
                                                          prefix="members_")
1158
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1159

    
1160
    modifications_table = None
1161
    if application.follower:
1162
        following_applications = list(application.followers())
1163
        following_applications.reverse()
1164
        modifications_table = \
1165
            tables.ProjectModificationApplicationsTable(following_applications,
1166
                                                       user=request.user,
1167
                                                       prefix="modifications_")
1168

    
1169
    return object_detail(
1170
        request,
1171
        queryset=ProjectApplication.objects.select_related(),
1172
        object_id=application_id,
1173
        template_name='im/projects/project_detail.html',
1174
        extra_context={
1175
            'addmembers_form':addmembers_form,
1176
            'members_table': members_table,
1177
            'user_owns_project': request.user.owns_project(application),
1178
            'modifications_table': modifications_table,
1179
            'member_status': application.user_status(request.user)
1180
            })
1181

    
1182
@require_http_methods(["GET", "POST"])
1183
@signed_terms_required
1184
@login_required
1185
def project_search(request):
1186
    q = request.GET.get('q', '')
1187
    form = ProjectSearchForm()
1188
    q = q.strip()
1189

    
1190
    if request.method == "POST":
1191
        form = ProjectSearchForm(request.POST)
1192
        if form.is_valid():
1193
            q = form.cleaned_data['q'].strip()
1194
        else:
1195
            q = None
1196

    
1197
    if q is None:
1198
        projects = ProjectApplication.objects.none()
1199
    else:
1200
        accepted_projects = request.user.projectmembership_set.filter(
1201
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1202
        projects = ProjectApplication.objects.search_by_name(q)
1203
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1204
        projects = projects.exclude(project__in=accepted_projects)
1205

    
1206
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1207
                                                prefix="my_projects_")
1208
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1209

    
1210
    return object_list(
1211
        request,
1212
        projects,
1213
        template_name='im/projects/project_list.html',
1214
        extra_context={
1215
          'form': form,
1216
          'is_search': True,
1217
          'q': q,
1218
          'table': table
1219
        })
1220

    
1221
@require_http_methods(["POST", "GET"])
1222
@signed_terms_required
1223
@login_required
1224
@transaction.commit_manually
1225
def project_join(request, application_id):
1226
    next = request.GET.get('next')
1227
    if not next:
1228
        next = reverse('astakos.im.views.project_detail',
1229
                       args=(application_id,))
1230

    
1231
    rollback = False
1232
    try:
1233
        application_id = int(application_id)
1234
        join_project(application_id, request.user)
1235
        # TODO: distinct messages for request/auto accept ???
1236
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1237
    except (IOError, PermissionDenied), e:
1238
        messages.error(request, e)
1239
    except BaseException, e:
1240
        logger.exception(e)
1241
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1242
        rollback = True
1243
    finally:
1244
        if rollback:
1245
            transaction.rollback()
1246
        else:
1247
            transaction.commit()
1248
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1249
    return redirect(next)
1250

    
1251
@require_http_methods(["POST"])
1252
@signed_terms_required
1253
@login_required
1254
@transaction.commit_manually
1255
def project_leave(request, application_id):
1256
    next = request.GET.get('next')
1257
    if not next:
1258
        next = reverse('astakos.im.views.project_list')
1259

    
1260
    rollback = False
1261
    try:
1262
        application_id = int(application_id)
1263
        leave_project(application_id, request.user)
1264
    except (IOError, PermissionDenied), e:
1265
        messages.error(request, e)
1266
    except BaseException, e:
1267
        logger.exception(e)
1268
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1269
        rollback = True
1270
    finally:
1271
        if rollback:
1272
            transaction.rollback()
1273
        else:
1274
            transaction.commit()
1275

    
1276
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1277
    return redirect(next)
1278

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

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

    
1333
@require_http_methods(["POST"])
1334
@signed_terms_required
1335
@login_required
1336
@transaction.commit_manually
1337
def project_reject_member(request, application_id, user_id):
1338
    rollback = False
1339
    try:
1340
        application_id = int(application_id)
1341
        user_id = int(user_id)
1342
        m = reject_membership(application_id, user_id, request.user)
1343
    except (IOError, PermissionDenied), e:
1344
        messages.error(request, e)
1345
    except BaseException, e:
1346
        logger.exception(e)
1347
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1348
        rollback = True
1349
    else:
1350
        realname = m.person.realname
1351
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1352
        messages.success(request, msg)
1353
    finally:
1354
        if rollback:
1355
            transaction.rollback()
1356
        else:
1357
            transaction.commit()
1358
    return redirect(reverse('project_detail', args=(application_id,)))
1359

    
1360