Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 6556e514

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

    
68
import astakos.im.messages as astakos_messages
69

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

    
103
logger = logging.getLogger(__name__)
104

    
105
callpoint = AstakosCallpoint()
106

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

    
121
def requires_auth_provider(provider_id, **perms):
122
    """
123
    """
124
    def decorator(func, *args, **kwargs):
125
        @wraps(func)
126
        def wrapper(request, *args, **kwargs):
127
            provider = auth_providers.get_provider(provider_id)
128

    
129
            if not provider or not provider.is_active():
130
                raise PermissionDenied
131

    
132
            if provider:
133
                for pkey, value in perms.iteritems():
134
                    attr = 'is_available_for_%s' % pkey.lower()
135
                    if getattr(provider, attr)() != value:
136
                        #TODO: add session message
137
                        return HttpResponseRedirect(reverse('login'))
138
            return func(request, *args)
139
        return wrapper
140
    return decorator
141

    
142

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

    
157

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

    
173

    
174
def required_auth_methods_assigned(only_warn=False):
175
    """
176
    Decorator that checks whether the request.user has all required auth providers
177
    assigned.
178
    """
179
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
180

    
181
    def decorator(func):
182
        if not required_providers:
183
            return func
184

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

    
201

    
202
def valid_astakos_user_required(func):
203
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
204

    
205

    
206
@require_http_methods(["GET", "POST"])
207
@signed_terms_required
208
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
209
    """
210
    If there is logged on user renders the profile page otherwise renders login page.
211

212
    **Arguments**
213

214
    ``login_template_name``
215
        A custom login template to use. This is optional; if not specified,
216
        this will default to ``im/login.html``.
217

218
    ``profile_template_name``
219
        A custom profile template to use. This is optional; if not specified,
220
        this will default to ``im/profile.html``.
221

222
    ``extra_context``
223
        An dictionary of variables to add to the template context.
224

225
    **Template:**
226

227
    im/profile.html or im/login.html or ``template_name`` keyword argument.
228

229
    """
230
    extra_context = extra_context or {}
231
    template_name = login_template_name
232
    if request.user.is_authenticated():
233
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
234

    
235
    third_party_token = request.GET.get('key', False)
236
    if third_party_token:
237
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
238

    
239
    return render_response(
240
        template_name,
241
        login_form = LoginForm(request=request),
242
        context_instance = get_context(request, extra_context)
243
    )
244

    
245

    
246
@require_http_methods(["GET", "POST"])
247
@valid_astakos_user_required
248
@transaction.commit_manually
249
def invite(request, template_name='im/invitations.html', extra_context=None):
250
    """
251
    Allows a user to invite somebody else.
252

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

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

260
    If the user isn't logged in, redirects to settings.LOGIN_URL.
261

262
    **Arguments**
263

264
    ``template_name``
265
        A custom template to use. This is optional; if not specified,
266
        this will default to ``im/invitations.html``.
267

268
    ``extra_context``
269
        An dictionary of variables to add to the template context.
270

271
    **Template:**
272

273
    im/invitations.html or ``template_name`` keyword argument.
274

275
    **Settings:**
276

277
    The view expectes the following settings are defined:
278

279
    * LOGIN_URL: login uri
280
    """
281
    extra_context = extra_context or {}
282
    status = None
283
    message = None
284
    form = InvitationForm()
285

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

    
312
    sent = [{'email': inv.username,
313
             'realname': inv.realname,
314
             'is_consumed': inv.is_consumed}
315
            for inv in request.user.invitations_sent.all()]
316
    kwargs = {'inviter': inviter,
317
              'sent': sent}
318
    context = get_context(request, extra_context, **kwargs)
319
    return render_response(template_name,
320
                           invitation_form=form,
321
                           context_instance=context)
322

    
323

    
324
@require_http_methods(["GET", "POST"])
325
@required_auth_methods_assigned(only_warn=True)
326
@login_required
327
@signed_terms_required
328
def edit_profile(request, template_name='im/profile.html', extra_context=None):
329
    """
330
    Allows a user to edit his/her profile.
331

332
    In case of GET request renders a form for displaying the user information.
333
    In case of POST updates the user informantion and redirects to ``next``
334
    url parameter if exists.
335

336
    If the user isn't logged in, redirects to settings.LOGIN_URL.
337

338
    **Arguments**
339

340
    ``template_name``
341
        A custom template to use. This is optional; if not specified,
342
        this will default to ``im/profile.html``.
343

344
    ``extra_context``
345
        An dictionary of variables to add to the template context.
346

347
    **Template:**
348

349
    im/profile.html or ``template_name`` keyword argument.
350

351
    **Settings:**
352

353
    The view expectes the following settings are defined:
354

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

    
391
    # existing providers
392
    user_providers = request.user.get_active_auth_providers()
393

    
394
    # providers that user can add
395
    user_available_providers = request.user.get_available_auth_providers()
396

    
397
    return render_response(template_name,
398
                           profile_form = form,
399
                           user_providers = user_providers,
400
                           user_available_providers = user_available_providers,
401
                           context_instance = get_context(request,
402
                                                          extra_context))
403

    
404

    
405
@transaction.commit_manually
406
@require_http_methods(["GET", "POST"])
407
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
408
    """
409
    Allows a user to create a local account.
410

411
    In case of GET request renders a form for entering the user information.
412
    In case of POST handles the signup.
413

414
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
415
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
416
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
417
    (see activation_backends);
418

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

422
    On unsuccessful creation, renders ``template_name`` with an error message.
423

424
    **Arguments**
425

426
    ``template_name``
427
        A custom template to render. This is optional;
428
        if not specified, this will default to ``im/signup.html``.
429

430
    ``on_success``
431
        A custom template to render in case of success. This is optional;
432
        if not specified, this will default to ``im/signup_complete.html``.
433

434
    ``extra_context``
435
        An dictionary of variables to add to the template context.
436

437
    **Template:**
438

439
    im/signup.html or ``template_name`` keyword argument.
440
    im/signup_complete.html or ``on_success`` keyword argument.
441
    """
442
    extra_context = extra_context or {}
443
    if request.user.is_authenticated():
444
        return HttpResponseRedirect(reverse('edit_profile'))
445

    
446
    provider = get_query(request).get('provider', 'local')
447
    if not auth_providers.get_provider(provider).is_available_for_create():
448
        raise PermissionDenied
449

    
450
    id = get_query(request).get('id')
451
    try:
452
        instance = AstakosUser.objects.get(id=id) if id else None
453
    except AstakosUser.DoesNotExist:
454
        instance = None
455

    
456
    third_party_token = request.REQUEST.get('third_party_token', None)
457
    if third_party_token:
458
        pending = get_object_or_404(PendingThirdPartyUser,
459
                                    token=third_party_token)
460
        provider = pending.provider
461
        instance = pending.get_user_instance()
462

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

    
478
                form.store_user(user, request)
479

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

    
521

    
522
@require_http_methods(["GET", "POST"])
523
@required_auth_methods_assigned(only_warn=True)
524
@login_required
525
@signed_terms_required
526
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
527
    """
528
    Allows a user to send feedback.
529

530
    In case of GET request renders a form for providing the feedback information.
531
    In case of POST sends an email to support team.
532

533
    If the user isn't logged in, redirects to settings.LOGIN_URL.
534

535
    **Arguments**
536

537
    ``template_name``
538
        A custom template to use. This is optional; if not specified,
539
        this will default to ``im/feedback.html``.
540

541
    ``extra_context``
542
        An dictionary of variables to add to the template context.
543

544
    **Template:**
545

546
    im/signup.html or ``template_name`` keyword argument.
547

548
    **Settings:**
549

550
    * LOGIN_URL: login uri
551
    """
552
    extra_context = extra_context or {}
553
    if request.method == 'GET':
554
        form = FeedbackForm()
555
    if request.method == 'POST':
556
        if not request.user:
557
            return HttpResponse('Unauthorized', status=401)
558

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

    
574

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

    
591
    next = restrict_next(
592
        request.GET.get('next'),
593
        domain=COOKIE_DOMAIN
594
    )
595

    
596
    if next:
597
        response['Location'] = next
598
        response.status_code = 302
599
    elif LOGOUT_NEXT:
600
        response['Location'] = LOGOUT_NEXT
601
        response.status_code = 301
602
    else:
603
        messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
604
        response['Location'] = reverse('index')
605
        response.status_code = 301
606
    return response
607

    
608

    
609
@require_http_methods(["GET", "POST"])
610
@transaction.commit_manually
611
def activate(request, greeting_email_template_name='im/welcome_email.txt',
612
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
613
    """
614
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
615
    and renews the user token.
616

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

    
627
    if user.is_active:
628
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
629
        messages.error(request, message)
630
        return index(request)
631

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

    
650

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

    
667
    if not term:
668
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
669
        return HttpResponseRedirect(reverse('index'))
670
    f = open(term.location, 'r')
671
    terms = f.read()
672

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

    
697

    
698
@require_http_methods(["GET", "POST"])
699
@valid_astakos_user_required
700
@transaction.commit_manually
701
def change_email(request, activation_key=None,
702
                 email_template_name='registration/email_change_email.txt',
703
                 form_template_name='registration/email_change_form.html',
704
                 confirm_template_name='registration/email_change_done.html',
705
                 extra_context=None):
706
    extra_context = extra_context or {}
707

    
708

    
709
    if activation_key:
710
        try:
711
            user = EmailChange.objects.change_email(activation_key)
712
            if request.user.is_authenticated() and request.user == user:
713
                msg = _(astakos_messages.EMAIL_CHANGED)
714
                messages.success(request, msg)
715
                auth_logout(request)
716
                response = prepare_response(request, user)
717
                transaction.commit()
718
                return HttpResponseRedirect(reverse('edit_profile'))
719
        except ValueError, e:
720
            messages.error(request, e)
721
            transaction.rollback()
722
            return HttpResponseRedirect(reverse('index'))
723

    
724
        return render_response(confirm_template_name,
725
                               modified_user=user if 'user' in locals() \
726
                               else None, context_instance=get_context(request,
727
                                                            extra_context))
728

    
729
    if not request.user.is_authenticated():
730
        path = quote(request.get_full_path())
731
        url = request.build_absolute_uri(reverse('index'))
732
        return HttpResponseRedirect(url + '?next=' + path)
733

    
734
    # clean up expired email changes
735
    if request.user.email_change_is_pending():
736
        change = request.user.emailchanges.get()
737
        if change.activation_key_expired():
738
            change.delete()
739
            transaction.commit()
740
            return HttpResponseRedirect(reverse('email_change'))
741

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

    
759
    if request.user.email_change_is_pending():
760
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
761

    
762
    return render_response(
763
        form_template_name,
764
        form=form,
765
        context_instance=get_context(request, extra_context)
766
    )
767

    
768

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

    
771
    if request.user.is_authenticated():
772
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
773
        return HttpResponseRedirect(reverse('edit_profile'))
774

    
775
    if astakos_settings.MODERATION_ENABLED:
776
        raise PermissionDenied
777

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

    
799

    
800
@require_http_methods(["GET"])
801
@valid_astakos_user_required
802
def resource_usage(request):
803

    
804
    def with_class(entry):
805
         entry['load_class'] = 'red'
806
         max_value = float(entry['maxValue'])
807
         curr_value = float(entry['currValue'])
808
         entry['ratio_limited']= 0
809
         if max_value > 0 :
810
             entry['ratio'] = (curr_value / max_value) * 100
811
         else:
812
             entry['ratio'] = 0
813
         if entry['ratio'] < 66:
814
             entry['load_class'] = 'yellow'
815
         if entry['ratio'] < 33:
816
             entry['load_class'] = 'green'
817
         if entry['ratio']<0:
818
             entry['ratio'] = 0
819
         if entry['ratio']>100:
820
             entry['ratio_limited'] = 100
821
         else:
822
             entry['ratio_limited'] = entry['ratio']
823
         return entry
824

    
825
    def pluralize(entry):
826
        entry['plural'] = engine.plural(entry.get('name'))
827
        return entry
828

    
829
    resource_usage = None
830
    result = callpoint.get_user_usage(request.user.id)
831
    if result.is_success:
832
        resource_usage = result.data
833
        backenddata = map(with_class, result.data)
834
        backenddata = map(pluralize , backenddata)
835
    else:
836
        messages.error(request, result.reason)
837
        backenddata = []
838
    return render_response('im/resource_usage.html',
839
                           context_instance=get_context(request),
840
                           resource_usage=backenddata,
841
                           result=result)
842

    
843
# TODO: action only on POST and user should confirm the removal
844
@require_http_methods(["GET", "POST"])
845
@login_required
846
@signed_terms_required
847
def remove_auth_provider(request, pk):
848
    try:
849
        provider = request.user.auth_providers.get(pk=pk)
850
    except AstakosUserAuthProvider.DoesNotExist:
851
        raise Http404
852

    
853
    if provider.can_remove():
854
        provider.delete()
855
        return HttpResponseRedirect(reverse('edit_profile'))
856
    else:
857
        raise PermissionDenied
858

    
859

    
860
def how_it_works(request):
861
    return render_response(
862
        'im/how_it_works.html',
863
        context_instance=get_context(request))
864

    
865
@transaction.commit_manually
866
def _create_object(request, model=None, template_name=None,
867
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
868
        login_required=False, context_processors=None, form_class=None,
869
        msg=None):
870
    """
871
    Based of django.views.generic.create_update.create_object which displays a
872
    summary page before creating the object.
873
    """
874
    rollback = False
875
    response = None
876

    
877
    if extra_context is None: extra_context = {}
878
    if login_required and not request.user.is_authenticated():
879
        return redirect_to_login(request.path)
880
    try:
881

    
882
        model, form_class = get_model_and_form_class(model, form_class)
883
        extra_context['edit'] = 0
884
        if request.method == 'POST':
885
            form = form_class(request.POST, request.FILES)
886
            if form.is_valid():
887
                verify = request.GET.get('verify')
888
                edit = request.GET.get('edit')
889
                if verify == '1':
890
                    extra_context['show_form'] = False
891
                    extra_context['form_data'] = form.cleaned_data
892
                elif edit == '1':
893
                    extra_context['show_form'] = True
894
                else:
895
                    new_object = form.save()
896
                    if not msg:
897
                        msg = _("The %(verbose_name)s was created successfully.")
898
                    msg = msg % model._meta.__dict__
899
                    messages.success(request, msg, fail_silently=True)
900
                    response = redirect(post_save_redirect, new_object)
901
        else:
902
            form = form_class()
903
    except BaseException, e:
904
        logger.exception(e)
905
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
906
        rollback = True
907
    finally:
908
        if rollback:
909
            transaction.rollback()
910
        else:
911
            transaction.commit()
912

    
913
        if response == None:
914
            # Create the template, context, response
915
            if not template_name:
916
                template_name = "%s/%s_form.html" %\
917
                     (model._meta.app_label, model._meta.object_name.lower())
918
            t = template_loader.get_template(template_name)
919
            c = RequestContext(request, {
920
                'form': form
921
            }, context_processors)
922
            apply_extra_context(extra_context, c)
923
            response = HttpResponse(t.render(c))
924
        return response
925

    
926
@transaction.commit_manually
927
def _update_object(request, model=None, object_id=None, slug=None,
928
        slug_field='slug', template_name=None, template_loader=template_loader,
929
        extra_context=None, post_save_redirect=None, login_required=False,
930
        context_processors=None, template_object_name='object',
931
        form_class=None, msg=None):
932
    """
933
    Based of django.views.generic.create_update.update_object which displays a
934
    summary page before updating the object.
935
    """
936
    rollback = False
937
    response = None
938

    
939
    if extra_context is None: extra_context = {}
940
    if login_required and not request.user.is_authenticated():
941
        return redirect_to_login(request.path)
942

    
943
    try:
944
        model, form_class = get_model_and_form_class(model, form_class)
945
        obj = lookup_object(model, object_id, slug, slug_field)
946

    
947
        if request.method == 'POST':
948
            form = form_class(request.POST, request.FILES, instance=obj)
949
            if form.is_valid():
950
                verify = request.GET.get('verify')
951
                edit = request.GET.get('edit')
952
                if verify == '1':
953
                    extra_context['show_form'] = False
954
                    extra_context['form_data'] = form.cleaned_data
955
                elif edit == '1':
956
                    extra_context['show_form'] = True
957
                else:
958
                    obj = form.save()
959
                    if not msg:
960
                        msg = _("The %(verbose_name)s was created successfully.")
961
                    msg = msg % model._meta.__dict__
962
                    messages.success(request, msg, fail_silently=True)
963
                    messages.success(request, msg, fail_silently=True)
964
                    response = redirect(post_save_redirect, obj)
965
        else:
966
            form = form_class(instance=obj)
967
    except BaseException, e:
968
        logger.exception(e)
969
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
970
        rollback = True
971
    finally:
972
        if rollback:
973
            transaction.rollback()
974
        else:
975
            transaction.commit()
976
        if response == None:
977
            if not template_name:
978
                template_name = "%s/%s_form.html" %\
979
                    (model._meta.app_label, model._meta.object_name.lower())
980
            t = template_loader.get_template(template_name)
981
            c = RequestContext(request, {
982
                'form': form,
983
                template_object_name: obj,
984
            }, context_processors)
985
            apply_extra_context(extra_context, c)
986
            response = HttpResponse(t.render(c))
987
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
988
        return response
989

    
990
@require_http_methods(["GET", "POST"])
991
@signed_terms_required
992
@login_required
993
def project_add(request):
994
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
995
    resource_catalog = ()
996
    result = callpoint.list_resources()
997
    details_fields = [
998
        "name", "homepage", "description","start_date","end_date", "comments"]
999
    membership_fields =[
1000
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1001
    if not result.is_success:
1002
        messages.error(
1003
            request,
1004
            'Unable to retrieve system resources: %s' % result.reason
1005
    )
1006
    else:
1007
        resource_catalog = [
1008
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1009
                for g in resource_groups]
1010
    extra_context = {
1011
        'resource_catalog':resource_catalog,
1012
        'resource_groups':resource_groups,
1013
        'show_form':True,
1014
        'details_fields':details_fields,
1015
        'membership_fields':membership_fields}
1016
    return _create_object(
1017
        request,
1018
        template_name='im/projects/projectapplication_form.html',
1019
        extra_context=extra_context,
1020
        post_save_redirect=reverse('project_list'),
1021
        form_class=ProjectApplicationForm,
1022
        msg=_("The %(verbose_name)s has been received and \
1023
                 is under consideration."))
1024

    
1025

    
1026
@require_http_methods(["GET"])
1027
@signed_terms_required
1028
@login_required
1029
def project_list(request):
1030
    q = ProjectApplication.objects.filter(owner=request.user)
1031
    q |= ProjectApplication.objects.filter(applicant=request.user)
1032
    q |= ProjectApplication.objects.filter(
1033
            project__in=request.user.projectmembership_set.values_list(
1034
                'project', flat=True))
1035
    q = q.select_related()
1036
    sorting = 'name'
1037
    sort_form = ProjectSortForm(request.GET)
1038
    if sort_form.is_valid():
1039
        sorting = sort_form.cleaned_data.get('sorting')
1040
    q = q.order_by(sorting)
1041

    
1042
    return object_list(
1043
        request,
1044
        q,
1045
        paginate_by=PAGINATE_BY,
1046
        page=request.GET.get('page') or 1,
1047
        template_name='im/projects/project_list.html',
1048
        extra_context={
1049
            'is_search':False,
1050
            'sorting':sorting
1051
        }
1052
    )
1053

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

    
1089

    
1090
@require_http_methods(["GET", "POST"])
1091
@signed_terms_required
1092
@login_required
1093
@transaction.commit_manually
1094
def project_detail(request, application_id):
1095
    addmembers_form = AddProjectMembersForm()
1096
    if request.method == 'POST':
1097
        addmembers_form = AddProjectMembersForm(request.POST)
1098
        if addmembers_form.is_valid():
1099
            try:
1100
                rollback = False
1101
                application_id = int(application_id)
1102
                map(lambda u: enroll_member(
1103
                        application_id,
1104
                        u,
1105
                        request_user=request.user),
1106
                    addmembers_form.valid_users)
1107
            except (IOError, PermissionDenied), e:
1108
                messages.error(request, e)
1109
            except BaseException, e:
1110
                rollback = True
1111
                messages.error(request, e)
1112
            finally:
1113
                if rollback == True:
1114
                    transaction.rollback()
1115
                else:
1116
                    transaction.commit()
1117
            addmembers_form = AddProjectMembersForm()
1118

    
1119
    # validate sorting
1120
    sorting = 'person__email'
1121
    form = ProjectMembersSortForm(request.GET or request.POST)
1122
    if form.is_valid():
1123
        sorting = form.cleaned_data.get('sorting')
1124

    
1125
    rollback = False
1126
    try:
1127
        return object_detail(
1128
            request,
1129
            queryset=ProjectApplication.objects.select_related(),
1130
            object_id=application_id,
1131
            template_name='im/projects/project_detail.html',
1132
            extra_context={
1133
                'sorting':sorting,
1134
                'addmembers_form':addmembers_form
1135
                }
1136
            )
1137
    except:
1138
        rollback = True
1139
    finally:
1140
        if rollback == True:
1141
            transaction.rollback()
1142
        else:
1143
            transaction.commit()
1144

    
1145
@require_http_methods(["GET", "POST"])
1146
@signed_terms_required
1147
@login_required
1148
def project_search(request):
1149
    q = request.GET.get('q', '')
1150
    queryset = ProjectApplication.objects
1151

    
1152
    if request.method == 'GET':
1153
        form = ProjectSearchForm()
1154
        q = q.strip()
1155
        queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1156
        queryset = queryset.filter(name__contains=q)
1157
    else:
1158
        form = ProjectSearchForm(request.POST)
1159

    
1160
        if form.is_valid():
1161
            q = form.cleaned_data['q'].strip()
1162

    
1163
            queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1164

    
1165
            queryset = queryset.filter(name__contains=q)
1166
        else:
1167
            queryset = queryset.none()
1168

    
1169
    sorting = 'name'
1170
    # validate sorting
1171
    sort_form = ProjectSortForm(request.GET)
1172
    if sort_form.is_valid():
1173
        sorting = sort_form.cleaned_data.get('sorting')
1174
    queryset = queryset.order_by(sorting)
1175
 
1176
    return object_list(
1177
        request,
1178
        queryset,
1179
        paginate_by=PAGINATE_BY_ALL,
1180
        page=request.GET.get('page') or 1,
1181
        template_name='im/projects/project_list.html',
1182
        extra_context=dict(
1183
            form=form,
1184
            is_search=True,
1185
            sorting=sorting,
1186
            q=q,
1187
        )
1188
    )
1189

    
1190
@require_http_methods(["POST"])
1191
@signed_terms_required
1192
@login_required
1193
@transaction.commit_manually
1194
def project_join(request, application_id):
1195
    next = request.GET.get('next')
1196
    if not next:
1197
        return HttpResponseBadRequest(
1198
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1199

    
1200
    rollback = False
1201
    try:
1202
        application_id = int(application_id)
1203
        join_project(application_id, request.user)
1204
    except (IOError, PermissionDenied), e:
1205
        messages.error(request, e)
1206
    except BaseException, e:
1207
        logger.exception(e)
1208
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1209
        rollback = True
1210
    finally:
1211
        if rollback:
1212
            transaction.rollback()
1213
        else:
1214
            transaction.commit()
1215
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1216
    return redirect(next)
1217

    
1218
@require_http_methods(["POST"])
1219
@signed_terms_required
1220
@login_required
1221
@transaction.commit_manually
1222
def project_leave(request, application_id):
1223
    next = request.GET.get('next')
1224
    if not next:
1225
        return HttpResponseBadRequest(
1226
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1227

    
1228
    rollback = False
1229
    try:
1230
        application_id = int(application_id)
1231
        leave_project(application_id, request.user)
1232
    except (IOError, PermissionDenied), e:
1233
        messages.error(request, e)
1234
    except BaseException, e:
1235
        logger.exception(e)
1236
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1237
        rollback = True
1238
    finally:
1239
        if rollback:
1240
            transaction.rollback()
1241
        else:
1242
            transaction.commit()
1243

    
1244
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1245
    return redirect(next)
1246

    
1247
@require_http_methods(["GET"])
1248
@signed_terms_required
1249
@login_required
1250
@transaction.commit_manually
1251
def project_accept_member(request, application_id, user_id):
1252
    rollback = False
1253
    try:
1254
        application_id = int(application_id)
1255
        user_id = int(user_id)
1256
        m = accept_membership(application_id, user_id, request.user)
1257
    except (IOError, PermissionDenied), e:
1258
        messages.error(request, e)
1259
    except BaseException, e:
1260
        logger.exception(e)
1261
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1262
        rollback = True
1263
    else:
1264
        realname = m.person.realname
1265
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1266
        messages.success(request, msg)
1267
    finally:
1268
        if rollback:
1269
            transaction.rollback()
1270
        else:
1271
            transaction.commit()
1272
    return redirect(reverse('project_detail', args=(application_id,)))
1273

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

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

    
1328