Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (45.9 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
44
from django.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
from astakos.im.templatetags.filters import ResourcePresentation
103

    
104
logger = logging.getLogger(__name__)
105

    
106
callpoint = AstakosCallpoint()
107

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

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

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

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

    
143

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

    
158

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

    
174

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

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

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

    
202

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

    
206

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

213
    **Arguments**
214

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

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

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

226
    **Template:**
227

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

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

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

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

    
246

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

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

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

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

263
    **Arguments**
264

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

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

272
    **Template:**
273

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

276
    **Settings:**
277

278
    The view expectes the following settings are defined:
279

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

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

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

    
324

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

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

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

339
    **Arguments**
340

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

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

348
    **Template:**
349

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

352
    **Settings:**
353

354
    The view expectes the following settings are defined:
355

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

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

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

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

    
405

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

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

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

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

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

425
    **Arguments**
426

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

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

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

438
    **Template:**
439

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

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

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

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

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

    
479
                form.store_user(user, request)
480

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

    
522

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

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

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

536
    **Arguments**
537

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

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

545
    **Template:**
546

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

549
    **Settings:**
550

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

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

    
575

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

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

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

    
609

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

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

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

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

    
651

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

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

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

    
698

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

    
709

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

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

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

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

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

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

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

    
769

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

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

    
776
    if astakos_settings.MODERATION_ENABLED:
777
        raise PermissionDenied
778

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

    
800

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

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

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

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

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

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

    
860

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

    
866
@transaction.commit_manually
867
def _create_object(request, model=None, template_name=None,
868
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
869
        login_required=False, context_processors=None, form_class=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

    
897
                    msg = _("The %(verbose_name)s has been received and is under consideration .") %\
898
                                {"verbose_name": model._meta.verbose_name}
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):
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
                    msg = _("The %(verbose_name)s has been received and is under consideration .") %\
960
                                {"verbose_name": model._meta.verbose_name}
961
                    messages.success(request, msg, fail_silently=True)
962
                    response = redirect(post_save_redirect, obj)
963
        else:
964
            form = form_class(instance=obj)
965
    except BaseException, e:
966
        logger.exception(e)
967
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
968
        rollback = True
969
    finally:
970
        if rollback:
971
            transaction.rollback()
972
        else:
973
            transaction.commit()
974
        if response == None:
975
            if not template_name:
976
                template_name = "%s/%s_form.html" %\
977
                    (model._meta.app_label, model._meta.object_name.lower())
978
            t = template_loader.get_template(template_name)
979
            c = RequestContext(request, {
980
                'form': form,
981
                template_object_name: obj,
982
            }, context_processors)
983
            apply_extra_context(extra_context, c)
984
            response = HttpResponse(t.render(c))
985
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
986
        return response
987

    
988
@require_http_methods(["GET", "POST"])
989
@signed_terms_required
990
@login_required
991
def project_add(request):
992
    result = callpoint.list_resources()
993
    details_fields = ["name", "homepage", "description","start_date","end_date", "comments"]
994
    membership_fields =["member_join_policy", "member_leave_policy", "limit_on_members_number"] 
995
    if not result.is_success:
996
        messages.error(
997
            request,
998
            'Unable to retrieve system resources: %s' % result.reason
999
    )
1000
    else:
1001
        resource_catalog = result.data
1002
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True, 'details_fields':details_fields, 'membership_fields':membership_fields}
1003
    return _create_object(request, template_name='im/projects/projectapplication_form.html',
1004
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1005
        form_class=ProjectApplicationForm) 
1006

    
1007

    
1008
@require_http_methods(["GET"])
1009
@signed_terms_required
1010
@login_required
1011
def project_list(request):
1012
    q = ProjectApplication.objects.filter(owner=request.user)
1013
    q |= ProjectApplication.objects.filter(applicant=request.user)
1014
    q |= ProjectApplication.objects.filter(
1015
        project__in=request.user.projectmembership_set.values_list('project', flat=True)
1016
    )
1017
    q = q.select_related()
1018
    sorting = 'name'
1019
    sort_form = ProjectSortForm(request.GET)
1020
    if sort_form.is_valid():
1021
        sorting = sort_form.cleaned_data.get('sorting')
1022
    q = q.order_by(sorting)
1023

    
1024
    return object_list(
1025
        request,
1026
        q,
1027
        paginate_by=PAGINATE_BY,
1028
        page=request.GET.get('page') or 1,
1029
        template_name='im/projects/project_list.html',
1030
        extra_context={
1031
            'is_search':False,
1032
            'sorting':sorting
1033
        }
1034
    )
1035

    
1036
@require_http_methods(["GET", "POST"])
1037
@signed_terms_required
1038
@login_required
1039
def project_update(request, application_id):
1040
    result = callpoint.list_resources()
1041
    details_fields = ["name", "homepage", "description","start_date","end_date", "comments"]
1042
    membership_fields =["member_join_policy", "member_leave_policy", "limit_on_members_number"] 
1043
    if not result.is_success:
1044
        messages.error(
1045
            request,
1046
            'Unable to retrieve system resources: %s' % result.reason
1047
    )
1048
    else:
1049
        resource_catalog = result.data
1050
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True, 'details_fields':details_fields, 'membership_fields':membership_fields}
1051
    return _update_object(
1052
        request,
1053
        object_id=application_id,
1054
        template_name='im/projects/projectapplication_form.html',
1055
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1056
        form_class=ProjectApplicationForm)
1057

    
1058

    
1059
@require_http_methods(["GET", "POST"])
1060
@signed_terms_required
1061
@login_required
1062
@transaction.commit_manually
1063
def project_detail(request, application_id):
1064
    resource_catalog = None
1065
    result = callpoint.list_resources()
1066
    if not result.is_success:
1067
        messages.error(
1068
            request,
1069
            'Unable to retrieve system resources: %s' % result.reason
1070
    )
1071
    else:
1072
        resource_catalog = result.data
1073

    
1074
    addmembers_form = AddProjectMembersForm()
1075
    if request.method == 'POST':
1076
        addmembers_form = AddProjectMembersForm(request.POST)
1077
        if addmembers_form.is_valid():
1078
            try:
1079
                rollback = False
1080
                application_id = int(application_id)
1081
                map(lambda u: enroll_member(
1082
                        application_id,
1083
                        u,
1084
                        request_user=request.user),
1085
                    addmembers_form.valid_users)
1086
            except (IOError, PermissionDenied), e:
1087
                messages.error(request, e)
1088
            except BaseException, e:
1089
                rollback = True
1090
                messages.error(request, e)
1091
            finally:
1092
                if rollback == True:
1093
                    transaction.rollback()
1094
                else:
1095
                    transaction.commit()
1096
            addmembers_form = AddProjectMembersForm()
1097

    
1098
    # validate sorting
1099
    sorting = 'person__email'
1100
    form = ProjectMembersSortForm(request.GET or request.POST)
1101
    if form.is_valid():
1102
        sorting = form.cleaned_data.get('sorting')
1103

    
1104
    rollback = False
1105
    try:
1106
        return object_detail(
1107
            request,
1108
            queryset=ProjectApplication.objects.select_related(),
1109
            object_id=application_id,
1110
            template_name='im/projects/project_detail.html',
1111
            extra_context={
1112
                'resource_catalog':resource_catalog,
1113
                'sorting':sorting,
1114
                'addmembers_form':addmembers_form
1115
                }
1116
            )
1117
    except:
1118
        rollback = True
1119
    finally:
1120
        if rollback == True:
1121
            transaction.rollback()
1122
        else:
1123
            transaction.commit()
1124

    
1125
@require_http_methods(["GET", "POST"])
1126
@signed_terms_required
1127
@login_required
1128
def project_search(request):
1129
    q = request.GET.get('q', '')
1130
    queryset = ProjectApplication.objects
1131

    
1132
    if request.method == 'GET':
1133
        form = ProjectSearchForm()
1134
        q = q.strip()
1135
        queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1136
        queryset = queryset.filter(name__contains=q)
1137
    else:
1138
        form = ProjectSearchForm(request.POST)
1139

    
1140
        if form.is_valid():
1141
            q = form.cleaned_data['q'].strip()
1142

    
1143
            queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1144

    
1145
            queryset = queryset.filter(name__contains=q)
1146
        else:
1147
            queryset = queryset.none()
1148

    
1149
    sorting = 'name'
1150
    # validate sorting
1151
    sort_form = ProjectSortForm(request.GET)
1152
    if sort_form.is_valid():
1153
        sorting = sort_form.cleaned_data.get('sorting')
1154
    queryset = queryset.order_by(sorting)
1155
 
1156
    return object_list(
1157
        request,
1158
        queryset,
1159
        paginate_by=PAGINATE_BY_ALL,
1160
        page=request.GET.get('page') or 1,
1161
        template_name='im/projects/project_list.html',
1162
        extra_context=dict(
1163
            form=form,
1164
            is_search=True,
1165
            sorting=sorting,
1166
            q=q,
1167
        )
1168
    )
1169

    
1170
@require_http_methods(["POST"])
1171
@signed_terms_required
1172
@login_required
1173
@transaction.commit_manually
1174
def project_join(request, application_id):
1175
    next = request.GET.get('next')
1176
    if not next:
1177
        return HttpResponseBadRequest(
1178
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1179

    
1180
    rollback = False
1181
    try:
1182
        application_id = int(application_id)
1183
        join_project(application_id, request.user)
1184
    except (IOError, PermissionDenied), e:
1185
        messages.error(request, e)
1186
    except BaseException, e:
1187
        logger.exception(e)
1188
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1189
        rollback = True
1190
    finally:
1191
        if rollback:
1192
            transaction.rollback()
1193
        else:
1194
            transaction.commit()
1195
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1196
    return redirect(next)
1197

    
1198
@require_http_methods(["POST"])
1199
@signed_terms_required
1200
@login_required
1201
@transaction.commit_manually
1202
def project_leave(request, application_id):
1203
    next = request.GET.get('next')
1204
    if not next:
1205
        return HttpResponseBadRequest(
1206
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1207

    
1208
    rollback = False
1209
    try:
1210
        application_id = int(application_id)
1211
        leave_project(application_id, request.user)
1212
    except (IOError, PermissionDenied), e:
1213
        messages.error(request, e)
1214
    except BaseException, e:
1215
        logger.exception(e)
1216
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1217
        rollback = True
1218
    finally:
1219
        if rollback:
1220
            transaction.rollback()
1221
        else:
1222
            transaction.commit()
1223

    
1224
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1225
    return redirect(next)
1226

    
1227
@require_http_methods(["GET"])
1228
@signed_terms_required
1229
@login_required
1230
@transaction.commit_manually
1231
def project_accept_member(request, application_id, user_id):
1232
    rollback = False
1233
    try:
1234
        application_id = int(application_id)
1235
        user_id = int(user_id)
1236
        m = accept_membership(application_id, user_id, request.user)
1237
    except (IOError, PermissionDenied), e:
1238
        messages.error(request, e)
1239
    except BaseException, e:
1240
        logger.exception(e)
1241
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1242
        rollback = True
1243
    else:
1244
        realname = m.person.realname
1245
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1246
        messages.success(request, msg)
1247
    finally:
1248
        if rollback:
1249
            transaction.rollback()
1250
        else:
1251
            transaction.commit()
1252
    return redirect(reverse('project_detail', args=(application_id,)))
1253

    
1254
@require_http_methods(["GET"])
1255
@signed_terms_required
1256
@login_required
1257
@transaction.commit_manually
1258
def project_remove_member(request, application_id, user_id):
1259
    rollback = False
1260
    try:
1261
        application_id = int(application_id)
1262
        user_id = int(user_id)
1263
        m = remove_membership(application_id, user_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
    else:
1271
        realname = m.person.realname
1272
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1273
        messages.success(request, msg)
1274
    finally:
1275
        if rollback:
1276
            transaction.rollback()
1277
        else:
1278
            transaction.commit()
1279
    return redirect(reverse('project_detail', args=(application_id,)))
1280

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

    
1308