Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ a5cef8d0

History | View | Annotate | Download (48.5 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

    
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime
43

    
44
from django_tables2 import RequestConfig
45

    
46
from django.shortcuts import get_object_or_404
47
from django.contrib import messages
48
from django.contrib.auth.decorators import login_required
49
from django.core.urlresolvers import reverse
50
from django.db import transaction
51
from django.db.utils import IntegrityError
52
from django.http import (
53
    HttpResponse, HttpResponseBadRequest,
54
    HttpResponseForbidden, HttpResponseRedirect,
55
    HttpResponseBadRequest, Http404)
56
from django.shortcuts import redirect
57
from django.template import RequestContext, loader as template_loader
58
from django.utils.http import urlencode
59
from django.utils.translation import ugettext as _
60
from django.views.generic.create_update import (
61
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
62
from django.views.generic.list_detail import object_list, object_detail
63
from django.core.xheaders import populate_xheaders
64
from django.core.exceptions import ValidationError, PermissionDenied
65
from django.template.loader import render_to_string
66
from django.views.decorators.http import require_http_methods
67
from django.db.models import Q
68
from django.core.exceptions import PermissionDenied
69

    
70
import astakos.im.messages as astakos_messages
71

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

    
109
logger = logging.getLogger(__name__)
110

    
111
callpoint = AstakosCallpoint()
112

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

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

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

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

    
148

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

    
163

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

    
179

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

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

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

    
207

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

    
211

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

218
    **Arguments**
219

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

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

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

231
    **Template:**
232

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

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

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

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

    
251

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

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

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

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

268
    **Arguments**
269

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

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

277
    **Template:**
278

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

281
    **Settings:**
282

283
    The view expectes the following settings are defined:
284

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

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

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

    
329

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

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

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

344
    **Arguments**
345

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

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

353
    **Template:**
354

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

357
    **Settings:**
358

359
    The view expectes the following settings are defined:
360

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

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

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

    
403
    return render_response(template_name,
404
                           profile_form = form,
405
                           user_providers = user_providers,
406
                           user_available_providers = user_available_providers,
407
                           context_instance = get_context(request,
408
                                                          extra_context))
409

    
410

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

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

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

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

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

430
    **Arguments**
431

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

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

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

443
    **Template:**
444

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

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

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

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

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

    
484
                form.store_user(user, request)
485

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

    
527

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

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

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

541
    **Arguments**
542

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

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

550
    **Template:**
551

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

554
    **Settings:**
555

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

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

    
580

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

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

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

    
614

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

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

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

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

    
656

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

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

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

    
703

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

    
714

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

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

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

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

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

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

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

    
774

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

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

    
781
    if astakos_settings.MODERATION_ENABLED:
782
        raise PermissionDenied
783

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

    
805

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

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

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

    
835
    resource_usage = None
836
    result = callpoint.get_user_usage(request.user.id)
837
    if result.is_success:
838
        resource_usage = result.data
839
        backenddata = map(with_class, result.data)
840
        backenddata = map(pluralize , backenddata)
841
    else:
842
        messages.error(request, result.reason)
843
        backenddata = []
844
    return render_response('im/resource_usage.html',
845
                           context_instance=get_context(request),
846
                           resource_usage=backenddata,
847
                           result=result)
848

    
849

    
850
##@require_http_methods(["GET"])
851
#@require_http_methods(["POST", "GET"])
852
#@signed_terms_required
853
#@login_required
854
#def billing(request):
855
#
856
#    today = datetime.today()
857
#    month_last_day = calendar.monthrange(today.year, today.month)[1]
858
#    start = request.POST.get('datefrom', None)
859
#    if start:
860
#        today = datetime.fromtimestamp(int(start))
861
#        month_last_day = calendar.monthrange(today.year, today.month)[1]
862
#
863
#    start = datetime(today.year, today.month, 1).strftime("%s")
864
#    end = datetime(today.year, today.month, month_last_day).strftime("%s")
865
#    r = request_billing.apply(args=('pgerakios@grnet.gr',
866
#                                    int(start) * 1000,
867
#                                    int(end) * 1000))
868
#    data = {}
869
#
870
#    try:
871
#        status, data = r.result
872
#        data = _clear_billing_data(data)
873
#        if status != 200:
874
#            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
875
#    except:
876
#        messages.error(request, r.result)
877
#
878
#    return render_response(
879
#        template='im/billing.html',
880
#        context_instance=get_context(request),
881
#        data=data,
882
#        zerodate=datetime(month=1, year=1970, day=1),
883
#        today=today,
884
#        start=int(start),
885
#        month_last_day=month_last_day)
886

    
887

    
888
#def _clear_billing_data(data):
889
#
890
#    # remove addcredits entries
891
#    def isnotcredit(e):
892
#        return e['serviceName'] != "addcredits"
893
#
894
#    # separate services
895
#    def servicefilter(service_name):
896
#        service = service_name
897
#
898
#        def fltr(e):
899
#            return e['serviceName'] == service
900
#        return fltr
901
#
902
#    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
903
#    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
904
#    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
905
#    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
906
#
907
#    return data
908

    
909

    
910
# #@require_http_methods(["GET"])
911
# @require_http_methods(["POST", "GET"])
912
# @signed_terms_required
913
# @login_required
914
# def timeline(request):
915
# #    data = {'entity':request.user.email}
916
#     timeline_body = ()
917
#     timeline_header = ()
918
# #    form = TimelineForm(data)
919
#     form = TimelineForm()
920
#     if request.method == 'POST':
921
#         data = request.POST
922
#         form = TimelineForm(data)
923
#         if form.is_valid():
924
#             data = form.cleaned_data
925
#             timeline_header = ('entity', 'resource',
926
#                                'event name', 'event date',
927
#                                'incremental cost', 'total cost')
928
#             timeline_body = timeline_charge(
929
#                 data['entity'], data['resource'],
930
#                 data['start_date'], data['end_date'],
931
#                 data['details'], data['operation'])
932
#
933
#     return render_response(template='im/timeline.html',
934
#                            context_instance=get_context(request),
935
#                            form=form,
936
#                            timeline_header=timeline_header,
937
#                            timeline_body=timeline_body)
938
#     return data
939

    
940

    
941
# TODO: action only on POST and user should confirm the removal
942
@require_http_methods(["GET", "POST"])
943
@login_required
944
@signed_terms_required
945
def remove_auth_provider(request, pk):
946
    try:
947
        provider = request.user.auth_providers.get(pk=pk)
948
    except AstakosUserAuthProvider.DoesNotExist:
949
        raise Http404
950

    
951
    if provider.can_remove():
952
        provider.delete()
953
        return HttpResponseRedirect(reverse('edit_profile'))
954
    else:
955
        raise PermissionDenied
956

    
957

    
958
def how_it_works(request):
959
    return render_response(
960
        'im/how_it_works.html',
961
        context_instance=get_context(request))
962

    
963
@transaction.commit_manually
964
def _create_object(request, model=None, template_name=None,
965
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
966
        login_required=False, context_processors=None, form_class=None ):
967
    """
968
    Based of django.views.generic.create_update.create_object which displays a
969
    summary page before creating the object.
970
    """
971
    rollback = False
972
    response = None
973

    
974
    if extra_context is None: extra_context = {}
975
    if login_required and not request.user.is_authenticated():
976
        return redirect_to_login(request.path)
977
    try:
978

    
979
        model, form_class = get_model_and_form_class(model, form_class)
980
        extra_context['edit'] = 0
981
        if request.method == 'POST':
982
            form = form_class(request.POST, request.FILES)
983
            if form.is_valid():
984
                verify = request.GET.get('verify')
985
                edit = request.GET.get('edit')
986
                if verify == '1':
987
                    extra_context['show_form'] = False
988
                    extra_context['form_data'] = form.cleaned_data
989
                elif edit == '1':
990
                    extra_context['show_form'] = True
991
                else:
992
                    new_object = form.save()
993

    
994
                    msg = _("The %(verbose_name)s has been received and is under consideration .") %\
995
                                {"verbose_name": model._meta.verbose_name}
996
                    messages.success(request, msg, fail_silently=True)
997
                    response = redirect(post_save_redirect, new_object)
998
        else:
999
            form = form_class()
1000
    except BaseException, e:
1001
        logger.exception(e)
1002
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1003
        rollback = True
1004
    finally:
1005
        if rollback:
1006
            transaction.rollback()
1007
        else:
1008
            transaction.commit()
1009

    
1010
        if response == None:
1011
            # Create the template, context, response
1012
            if not template_name:
1013
                template_name = "%s/%s_form.html" %\
1014
                     (model._meta.app_label, model._meta.object_name.lower())
1015
            t = template_loader.get_template(template_name)
1016
            c = RequestContext(request, {
1017
                'form': form
1018
            }, context_processors)
1019
            apply_extra_context(extra_context, c)
1020
            response = HttpResponse(t.render(c))
1021
        return response
1022

    
1023
@transaction.commit_manually
1024
def _update_object(request, model=None, object_id=None, slug=None,
1025
        slug_field='slug', template_name=None, template_loader=template_loader,
1026
        extra_context=None, post_save_redirect=None, login_required=False,
1027
        context_processors=None, template_object_name='object',
1028
        form_class=None):
1029
    """
1030
    Based of django.views.generic.create_update.update_object which displays a
1031
    summary page before updating the object.
1032
    """
1033
    rollback = False
1034
    response = None
1035

    
1036
    if extra_context is None: extra_context = {}
1037
    if login_required and not request.user.is_authenticated():
1038
        return redirect_to_login(request.path)
1039

    
1040
    try:
1041
        model, form_class = get_model_and_form_class(model, form_class)
1042
        obj = lookup_object(model, object_id, slug, slug_field)
1043

    
1044
        if request.method == 'POST':
1045
            form = form_class(request.POST, request.FILES, instance=obj)
1046
            if form.is_valid():
1047
                verify = request.GET.get('verify')
1048
                edit = request.GET.get('edit')
1049
                if verify == '1':
1050
                    extra_context['show_form'] = False
1051
                    extra_context['form_data'] = form.cleaned_data
1052
                elif edit == '1':
1053
                    extra_context['show_form'] = True
1054
                else:
1055
                    obj = form.save()
1056
                    msg = _("The %(verbose_name)s has been received and is under consideration .") %\
1057
                                {"verbose_name": model._meta.verbose_name}
1058
                    messages.success(request, msg, fail_silently=True)
1059
                    response = redirect(post_save_redirect, obj)
1060
        else:
1061
            form = form_class(instance=obj)
1062
    except BaseException, e:
1063
        logger.exception(e)
1064
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1065
        rollback = True
1066
    finally:
1067
        if rollback:
1068
            transaction.rollback()
1069
        else:
1070
            transaction.commit()
1071
        if response == None:
1072
            if not template_name:
1073
                template_name = "%s/%s_form.html" %\
1074
                    (model._meta.app_label, model._meta.object_name.lower())
1075
            t = template_loader.get_template(template_name)
1076
            c = RequestContext(request, {
1077
                'form': form,
1078
                template_object_name: obj,
1079
            }, context_processors)
1080
            apply_extra_context(extra_context, c)
1081
            response = HttpResponse(t.render(c))
1082
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1083
        return response
1084

    
1085
@require_http_methods(["GET", "POST"])
1086
@signed_terms_required
1087
@login_required
1088
def project_add(request):
1089
    result = callpoint.list_resources()
1090
    details_fields = ["name", "homepage", "description","start_date","end_date", "comments"]
1091
    membership_fields =["member_join_policy", "member_leave_policy", "limit_on_members_number"]
1092
    if not result.is_success:
1093
        messages.error(
1094
            request,
1095
            'Unable to retrieve system resources: %s' % result.reason
1096
    )
1097
    else:
1098
        resource_catalog = result.data
1099
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True, 'details_fields':details_fields, 'membership_fields':membership_fields}
1100
    return _create_object(request, template_name='im/projects/projectapplication_form.html',
1101
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1102
        form_class=ProjectApplicationForm)
1103

    
1104

    
1105
@require_http_methods(["GET"])
1106
@signed_terms_required
1107
@login_required
1108
def project_list(request):
1109
    projects = ProjectApplication.objects.user_projects(request.user).select_related()
1110
    table = tables.UserProjectApplicationsTable(projects, user=request.user, prefix="my_projects")
1111
    RequestConfig(request).configure(table)
1112

    
1113
    return object_list(
1114
        request,
1115
        projects,
1116
        template_name='im/projects/project_list.html',
1117
        extra_context={
1118
            'is_search':False,
1119
            'table': table,
1120
        }
1121
    )
1122

    
1123
@require_http_methods(["GET", "POST"])
1124
@signed_terms_required
1125
@login_required
1126
def project_update(request, application_id):
1127
    result = callpoint.list_resources()
1128
    details_fields = ["name", "homepage", "description","start_date","end_date", "comments"]
1129
    membership_fields =["member_join_policy", "member_leave_policy", "limit_on_members_number"]
1130
    if not result.is_success:
1131
        messages.error(
1132
            request,
1133
            'Unable to retrieve system resources: %s' % result.reason
1134
    )
1135
    else:
1136
        resource_catalog = result.data
1137
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True,
1138
                     'details_fields':details_fields,
1139
                     'membership_fields':membership_fields}
1140
    return _update_object(
1141
        request,
1142
        object_id=application_id,
1143
        template_name='im/projects/projectapplication_form.html',
1144
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1145
        form_class=ProjectApplicationForm)
1146

    
1147

    
1148
@require_http_methods(["GET", "POST"])
1149
@signed_terms_required
1150
@login_required
1151
@transaction.commit_manually
1152
def project_detail(request, application_id):
1153
    resource_catalog = None
1154
    result = callpoint.list_resources()
1155
    if not result.is_success:
1156
        messages.error(
1157
            request,
1158
            'Unable to retrieve system resources: %s' % result.reason
1159
    )
1160
    else:
1161
        resource_catalog = result.data
1162

    
1163
    addmembers_form = AddProjectMembersForm()
1164
    if request.method == 'POST':
1165
        addmembers_form = AddProjectMembersForm(request.POST)
1166
        if addmembers_form.is_valid():
1167
            try:
1168
                rollback = False
1169
                application_id = int(application_id)
1170
                map(lambda u: enroll_member(
1171
                        application_id,
1172
                        u,
1173
                        request_user=request.user),
1174
                    addmembers_form.valid_users)
1175
            except (IOError, PermissionDenied), e:
1176
                messages.error(request, e)
1177
            except BaseException, e:
1178
                rollback = True
1179
                messages.error(request, e)
1180
            finally:
1181
                if rollback == True:
1182
                    transaction.rollback()
1183
                else:
1184
                    transaction.commit()
1185
            addmembers_form = AddProjectMembersForm()
1186

    
1187
    # validate sorting
1188
    sorting = 'person__email'
1189
    form = ProjectMembersSortForm(request.GET or request.POST)
1190
    if form.is_valid():
1191
        sorting = form.cleaned_data.get('sorting')
1192

    
1193
    rollback = False
1194
    try:
1195
        return object_detail(
1196
            request,
1197
            queryset=ProjectApplication.objects.select_related(),
1198
            object_id=application_id,
1199
            template_name='im/projects/project_detail.html',
1200
            extra_context={
1201
                'resource_catalog':resource_catalog,
1202
                'sorting':sorting,
1203
                'addmembers_form':addmembers_form
1204
                }
1205
            )
1206
    except:
1207
        rollback = True
1208
    finally:
1209
        if rollback == True:
1210
            transaction.rollback()
1211
        else:
1212
            transaction.commit()
1213

    
1214

    
1215
@require_http_methods(["GET", "POST"])
1216
@signed_terms_required
1217
@login_required
1218
def project_search(request):
1219
    q = request.GET.get('q', '')
1220
    form = ProjectSearchForm()
1221
    q = q.strip()
1222

    
1223
    if request.method == "POST":
1224
        form = ProjectSearchForm(request.POST)
1225
        if form.is_valid():
1226
            q = form.cleaned_data['q'].strip()
1227
        else:
1228
            q = None
1229

    
1230
    if q is None:
1231
        projects = ProjectApplication.objects.none()
1232
    else:
1233
        projects = ProjectApplication.objects.search_by_name(q)
1234
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1235

    
1236
    table = tables.UserProjectApplicationsTable(projects, user=request.user, prefix="my_projects")
1237
    RequestConfig(request).configure(table)
1238

    
1239
    return object_list(
1240
        request,
1241
        projects,
1242
        template_name='im/projects/project_list.html',
1243
        extra_context={
1244
          'form': form,
1245
          'is_search': True,
1246
          'q': q,
1247
          'table': table
1248
        })
1249

    
1250
@require_http_methods(["POST"])
1251
@signed_terms_required
1252
@login_required
1253
@transaction.commit_manually
1254
def project_join(request, application_id):
1255
    next = request.GET.get('next')
1256
    if not next:
1257
        return HttpResponseBadRequest(
1258
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1259

    
1260
    rollback = False
1261
    try:
1262
        application_id = int(application_id)
1263
        join_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
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1276
    return redirect(next)
1277

    
1278
@require_http_methods(["POST"])
1279
@signed_terms_required
1280
@login_required
1281
@transaction.commit_manually
1282
def project_leave(request, application_id):
1283
    next = request.GET.get('next')
1284
    if not next:
1285
        return HttpResponseBadRequest(
1286
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1287

    
1288
    rollback = False
1289
    try:
1290
        application_id = int(application_id)
1291
        leave_project(application_id, request.user)
1292
    except (IOError, PermissionDenied), e:
1293
        messages.error(request, e)
1294
    except BaseException, e:
1295
        logger.exception(e)
1296
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1297
        rollback = True
1298
    finally:
1299
        if rollback:
1300
            transaction.rollback()
1301
        else:
1302
            transaction.commit()
1303

    
1304
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1305
    return redirect(next)
1306

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

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

    
1361
@require_http_methods(["GET"])
1362
@signed_terms_required
1363
@login_required
1364
@transaction.commit_manually
1365
def project_reject_member(request, application_id, user_id):
1366
    rollback = False
1367
    try:
1368
        application_id = int(application_id)
1369
        user_id = int(user_id)
1370
        m = reject_membership(application_id, user_id, request.user)
1371
    except (IOError, PermissionDenied), e:
1372
        messages.error(request, e)
1373
    except BaseException, e:
1374
        logger.exception(e)
1375
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1376
        rollback = True
1377
    else:
1378
        realname = m.person.realname
1379
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1380
        messages.success(request, msg)
1381
    finally:
1382
        if rollback:
1383
            transaction.rollback()
1384
        else:
1385
            transaction.commit()
1386
    return redirect(reverse('project_detail', args=(application_id,)))
1387

    
1388