Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (48.1 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
44
from django_tables2 import RequestConfig
45

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

    
72
import astakos.im.messages as astakos_messages
73

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

    
110
logger = logging.getLogger(__name__)
111

    
112
callpoint = AstakosCallpoint()
113

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

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

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

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

    
149

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

    
164

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

    
180

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

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

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

    
208

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

    
212

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

219
    **Arguments**
220

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

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

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

232
    **Template:**
233

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

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

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

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

    
252

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

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

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

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

269
    **Arguments**
270

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

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

278
    **Template:**
279

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

282
    **Settings:**
283

284
    The view expectes the following settings are defined:
285

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

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

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

    
330

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

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

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

345
    **Arguments**
346

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

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

354
    **Template:**
355

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

358
    **Settings:**
359

360
    The view expectes the following settings are defined:
361

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

    
400
    # existing providers
401
    user_providers = request.user.get_active_auth_providers()
402

    
403
    # providers that user can add
404
    user_available_providers = request.user.get_available_auth_providers()
405

    
406
    extra_context['services'] = get_services_dict()
407
    return render_response(template_name,
408
                           profile_form = form,
409
                           user_providers = user_providers,
410
                           user_available_providers = user_available_providers,
411
                           context_instance = get_context(request,
412
                                                          extra_context))
413

    
414

    
415
@transaction.commit_manually
416
@require_http_methods(["GET", "POST"])
417
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
418
    """
419
    Allows a user to create a local account.
420

421
    In case of GET request renders a form for entering the user information.
422
    In case of POST handles the signup.
423

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

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

432
    On unsuccessful creation, renders ``template_name`` with an error message.
433

434
    **Arguments**
435

436
    ``template_name``
437
        A custom template to render. This is optional;
438
        if not specified, this will default to ``im/signup.html``.
439

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

443
    ``on_success``
444
        Resolvable view name to redirect on registration success.
445

446
    **Template:**
447

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

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

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

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

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

    
482
            # delete previously unverified accounts
483
            if AstakosUser.objects.user_exists(user.email):
484
                AstakosUser.objects.get_by_identifier(user.email).delete()
485

    
486
            try:
487
                result = backend.handle_activation(user)
488
                status = messages.SUCCESS
489
                message = result.message
490

    
491
                form.store_user(user, request)
492

    
493
                if 'additional_email' in form.cleaned_data:
494
                    additional_email = form.cleaned_data['additional_email']
495
                    if additional_email != user.email:
496
                        user.additionalmail_set.create(email=additional_email)
497
                        msg = 'Additional email: %s saved for user %s.' % (
498
                            additional_email,
499
                            user.email
500
                        )
501
                        logger._log(LOGGING_LEVEL, msg, [])
502

    
503
                if user and user.is_active:
504
                    next = request.POST.get('next', '')
505
                    response = prepare_response(request, user, next=next)
506
                    transaction.commit()
507
                    return response
508

    
509
                transaction.commit()
510
                messages.add_message(request, status, message)
511
                return HttpResponseRedirect(reverse(on_success))
512

    
513
            except SendMailError, e:
514
                logger.exception(e)
515
                status = messages.ERROR
516
                message = e.message
517
                messages.error(request, message)
518
                transaction.rollback()
519
            except BaseException, e:
520
                logger.exception(e)
521
                message = _(astakos_messages.GENERIC_ERROR)
522
                messages.error(request, message)
523
                logger.exception(e)
524
                transaction.rollback()
525

    
526
    return render_response(template_name,
527
                           signup_form=form,
528
                           third_party_token=third_party_token,
529
                           provider=provider,
530
                           context_instance=get_context(request, extra_context))
531

    
532

    
533
@require_http_methods(["GET", "POST"])
534
@required_auth_methods_assigned(only_warn=True)
535
@login_required
536
@signed_terms_required
537
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
538
    """
539
    Allows a user to send feedback.
540

541
    In case of GET request renders a form for providing the feedback information.
542
    In case of POST sends an email to support team.
543

544
    If the user isn't logged in, redirects to settings.LOGIN_URL.
545

546
    **Arguments**
547

548
    ``template_name``
549
        A custom template to use. This is optional; if not specified,
550
        this will default to ``im/feedback.html``.
551

552
    ``extra_context``
553
        An dictionary of variables to add to the template context.
554

555
    **Template:**
556

557
    im/signup.html or ``template_name`` keyword argument.
558

559
    **Settings:**
560

561
    * LOGIN_URL: login uri
562
    """
563
    extra_context = extra_context or {}
564
    if request.method == 'GET':
565
        form = FeedbackForm()
566
    if request.method == 'POST':
567
        if not request.user:
568
            return HttpResponse('Unauthorized', status=401)
569

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

    
585

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

    
602
    next = restrict_next(
603
        request.GET.get('next'),
604
        domain=COOKIE_DOMAIN
605
    )
606

    
607
    if next:
608
        response['Location'] = next
609
        response.status_code = 302
610
    elif LOGOUT_NEXT:
611
        response['Location'] = LOGOUT_NEXT
612
        response.status_code = 301
613
    else:
614
        message = _(astakos_messages.LOGOUT_SUCCESS)
615
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
616
        if last_provider:
617
            provider = auth_providers.get_provider(last_provider)
618
            extra_message = provider.get_logout_message_display
619
            if extra_message:
620
                message += '<br />' + extra_message
621
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
622
        response['Location'] = reverse('index')
623
        response.status_code = 301
624
    return response
625

    
626

    
627
@require_http_methods(["GET", "POST"])
628
@transaction.commit_manually
629
def activate(request, greeting_email_template_name='im/welcome_email.txt',
630
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
631
    """
632
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
633
    and renews the user token.
634

635
    The view uses commit_manually decorator in order to ensure the user state will be updated
636
    only if the email will be send successfully.
637
    """
638
    token = request.GET.get('auth')
639
    next = request.GET.get('next')
640
    try:
641
        user = AstakosUser.objects.get(auth_token=token)
642
    except AstakosUser.DoesNotExist:
643
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
644

    
645
    if user.is_active:
646
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
647
        messages.error(request, message)
648
        return index(request)
649

    
650
    try:
651
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
652
        response = prepare_response(request, user, next, renew=True)
653
        transaction.commit()
654
        return response
655
    except SendMailError, e:
656
        message = e.message
657
        messages.add_message(request, messages.ERROR, message)
658
        transaction.rollback()
659
        return index(request)
660
    except BaseException, e:
661
        status = messages.ERROR
662
        message = _(astakos_messages.GENERIC_ERROR)
663
        messages.add_message(request, messages.ERROR, message)
664
        logger.exception(e)
665
        transaction.rollback()
666
        return index(request)
667

    
668

    
669
@require_http_methods(["GET", "POST"])
670
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
671
    extra_context = extra_context or {}
672
    term = None
673
    terms = None
674
    if not term_id:
675
        try:
676
            term = ApprovalTerms.objects.order_by('-id')[0]
677
        except IndexError:
678
            pass
679
    else:
680
        try:
681
            term = ApprovalTerms.objects.get(id=term_id)
682
        except ApprovalTerms.DoesNotExist, e:
683
            pass
684

    
685
    if not term:
686
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
687
        return HttpResponseRedirect(reverse('index'))
688
    f = open(term.location, 'r')
689
    terms = f.read()
690

    
691
    if request.method == 'POST':
692
        next = restrict_next(
693
            request.POST.get('next'),
694
            domain=COOKIE_DOMAIN
695
        )
696
        if not next:
697
            next = reverse('index')
698
        form = SignApprovalTermsForm(request.POST, instance=request.user)
699
        if not form.is_valid():
700
            return render_response(template_name,
701
                                   terms=terms,
702
                                   approval_terms_form=form,
703
                                   context_instance=get_context(request, extra_context))
704
        user = form.save()
705
        return HttpResponseRedirect(next)
706
    else:
707
        form = None
708
        if request.user.is_authenticated() and not request.user.signed_terms:
709
            form = SignApprovalTermsForm(instance=request.user)
710
        return render_response(template_name,
711
                               terms=terms,
712
                               approval_terms_form=form,
713
                               context_instance=get_context(request, extra_context))
714

    
715

    
716
@require_http_methods(["GET", "POST"])
717
@valid_astakos_user_required
718
@transaction.commit_manually
719
def change_email(request, activation_key=None,
720
                 email_template_name='registration/email_change_email.txt',
721
                 form_template_name='registration/email_change_form.html',
722
                 confirm_template_name='registration/email_change_done.html',
723
                 extra_context=None):
724
    extra_context = extra_context or {}
725

    
726

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

    
742
        return render_response(confirm_template_name,
743
                               modified_user=user if 'user' in locals() \
744
                               else None, context_instance=get_context(request,
745
                                                            extra_context))
746

    
747
    if not request.user.is_authenticated():
748
        path = quote(request.get_full_path())
749
        url = request.build_absolute_uri(reverse('index'))
750
        return HttpResponseRedirect(url + '?next=' + path)
751

    
752
    # clean up expired email changes
753
    if request.user.email_change_is_pending():
754
        change = request.user.emailchanges.get()
755
        if change.activation_key_expired():
756
            change.delete()
757
            transaction.commit()
758
            return HttpResponseRedirect(reverse('email_change'))
759

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

    
777
    if request.user.email_change_is_pending():
778
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
779

    
780
    return render_response(
781
        form_template_name,
782
        form=form,
783
        context_instance=get_context(request, extra_context)
784
    )
785

    
786

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

    
789
    if request.user.is_authenticated():
790
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
791
        return HttpResponseRedirect(reverse('edit_profile'))
792

    
793
    if astakos_settings.MODERATION_ENABLED:
794
        raise PermissionDenied
795

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

    
817

    
818
@require_http_methods(["GET"])
819
@valid_astakos_user_required
820
def resource_usage(request):
821

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

    
843
    def pluralize(entry):
844
        entry['plural'] = engine.plural(entry.get('name'))
845
        return entry
846

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

    
858
    if request.REQUEST.get('json', None):
859
        return HttpResponse(json.dumps(backenddata),
860
                            mimetype="application/json")
861

    
862
    return render_response('im/resource_usage.html',
863
                           context_instance=get_context(request),
864
                           resource_usage=backenddata,
865
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
866
                           result=result)
867

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

    
878
    if provider.can_remove():
879
        provider.delete()
880
        return HttpResponseRedirect(reverse('edit_profile'))
881
    else:
882
        raise PermissionDenied
883

    
884

    
885
def how_it_works(request):
886
    return render_response(
887
        'im/how_it_works.html',
888
        context_instance=get_context(request))
889

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

    
902
    if extra_context is None: extra_context = {}
903
    if login_required and not request.user.is_authenticated():
904
        return redirect_to_login(request.path)
905
    try:
906

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

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

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

    
964
    if extra_context is None: extra_context = {}
965
    if login_required and not request.user.is_authenticated():
966
        return redirect_to_login(request.path)
967

    
968
    try:
969
        model, form_class = get_model_and_form_class(model, form_class)
970
        obj = lookup_object(model, object_id, slug, slug_field)
971

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

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

    
1049

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

    
1059
    return object_list(
1060
        request,
1061
        projects,
1062
        template_name='im/projects/project_list.html',
1063
        extra_context={
1064
            'is_search':False,
1065
            'table': table,
1066
        })
1067

    
1068

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

    
1105

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

    
1138
    rollback = False
1139

    
1140
    application = get_object_or_404(ProjectApplication, pk=application_id)
1141
    try:
1142
        members = application.project.projectmembership_set.select_related()
1143
    except Project.DoesNotExist:
1144
        members = ProjectMembership.objects.none()
1145

    
1146
    members_table = tables.ProjectApplicationMembersTable(application,
1147
                                                          members,
1148
                                                          user=request.user,
1149
                                                          prefix="members_")
1150
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1151

    
1152
    modifications_table = None
1153
    if application.follower:
1154
        following_applications = list(application.followers())
1155
        following_applications.reverse()
1156
        modifications_table = \
1157
            tables.ProjectModificationApplicationsTable(following_applications,
1158
                                                       user=request.user,
1159
                                                       prefix="modifications_")
1160

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

    
1174
@require_http_methods(["GET", "POST"])
1175
@signed_terms_required
1176
@login_required
1177
def project_search(request):
1178
    q = request.GET.get('q', '')
1179
    form = ProjectSearchForm()
1180
    q = q.strip()
1181

    
1182
    if request.method == "POST":
1183
        form = ProjectSearchForm(request.POST)
1184
        if form.is_valid():
1185
            q = form.cleaned_data['q'].strip()
1186
        else:
1187
            q = None
1188

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

    
1198
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1199
                                                prefix="my_projects_")
1200
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1201

    
1202
    return object_list(
1203
        request,
1204
        projects,
1205
        template_name='im/projects/project_list.html',
1206
        extra_context={
1207
          'form': form,
1208
          'is_search': True,
1209
          'q': q,
1210
          'table': table
1211
        })
1212

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

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

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

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

    
1268
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1269
    return redirect(next)
1270

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

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

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

    
1352