Statistics
| Branch: | Tag: | Revision:

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

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

    
67
from astakos.im.activation_backends import get_backend, SimpleBackend
68
from astakos.im.models import (
69
    AstakosUser, ApprovalTerms, AstakosGroup,
70
    EmailChange, GroupKind, Membership,
71
    RESOURCE_SEPARATOR, AstakosUserAuthProvider,
72
    ProjectApplication, ProjectMembership, Project
73
)
74
from astakos.im.util import get_context, prepare_response, get_query, restrict_next
75
from astakos.im.forms import (
76
    LoginForm, InvitationForm, ProfileForm,
77
    FeedbackForm, SignApprovalTermsForm,
78
    EmailChangeForm,
79
    AstakosGroupCreationForm, AstakosGroupSearchForm,
80
    AstakosGroupUpdateForm, AddGroupMembersForm,
81
    MembersSortForm, AstakosGroupSortForm,
82
    TimelineForm, PickResourceForm,
83
    AstakosGroupCreationSummaryForm,
84
    ProjectApplicationForm, ProjectSortForm,
85
    AddProjectMembersForm, ProjectGroupSearchForm
86
)
87
from astakos.im.functions import (
88
    send_feedback, SendMailError,
89
    logout as auth_logout,
90
    activate as activate_func,
91
    send_activation as send_activation_func,
92
    send_group_creation_notification,
93
    SendNotificationError)
94
from astakos.im.endpoints.qh import timeline_charge
95
from astakos.im.settings import (
96
    COOKIE_DOMAIN, LOGOUT_NEXT,
97
    LOGGING_LEVEL, PAGINATE_BY,
98
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL
99
)
100
#from astakos.im.tasks import request_billing
101
from astakos.im.api.callpoint import AstakosCallpoint
102
from astakos.im.notifications import NotificationError
103

    
104
import astakos.im.messages as astakos_messages
105
from astakos.im import settings
106
from astakos.im import auth_providers
107

    
108
logger = logging.getLogger(__name__)
109

    
110
callpoint = AstakosCallpoint()
111

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

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

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

    
137
            if provider:
138
                for pkey, value in perms.iteritems():
139
                    attr = 'is_available_for_%s' % pkey.lower()
140
                    if getattr(provider, attr)() != value:
141
                        raise PermissionDenied
142
            return func(request, *args)
143
        return wrapper
144
    return decorator
145

    
146

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

    
161

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

    
177

    
178
@require_http_methods(["GET", "POST"])
179
@signed_terms_required
180
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
181
    """
182
    If there is logged on user renders the profile page otherwise renders login page.
183

184
    **Arguments**
185

186
    ``login_template_name``
187
        A custom login template to use. This is optional; if not specified,
188
        this will default to ``im/login.html``.
189

190
    ``profile_template_name``
191
        A custom profile template to use. This is optional; if not specified,
192
        this will default to ``im/profile.html``.
193

194
    ``extra_context``
195
        An dictionary of variables to add to the template context.
196

197
    **Template:**
198

199
    im/profile.html or im/login.html or ``template_name`` keyword argument.
200

201
    """
202
    extra_context = extra_context or {}
203
    template_name = login_template_name
204
    if request.user.is_authenticated():
205
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
206

    
207
    return render_response(
208
        template_name,
209
        login_form = LoginForm(request=request),
210
        context_instance = get_context(request, extra_context)
211
    )
212

    
213

    
214
@require_http_methods(["GET", "POST"])
215
@login_required
216
@signed_terms_required
217
@transaction.commit_manually
218
def invite(request, template_name='im/invitations.html', extra_context=None):
219
    """
220
    Allows a user to invite somebody else.
221

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

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

229
    If the user isn't logged in, redirects to settings.LOGIN_URL.
230

231
    **Arguments**
232

233
    ``template_name``
234
        A custom template to use. This is optional; if not specified,
235
        this will default to ``im/invitations.html``.
236

237
    ``extra_context``
238
        An dictionary of variables to add to the template context.
239

240
    **Template:**
241

242
    im/invitations.html or ``template_name`` keyword argument.
243

244
    **Settings:**
245

246
    The view expectes the following settings are defined:
247

248
    * LOGIN_URL: login uri
249
    """
250
    extra_context = extra_context or {}
251
    status = None
252
    message = None
253
    form = InvitationForm()
254

    
255
    inviter = request.user
256
    if request.method == 'POST':
257
        form = InvitationForm(request.POST)
258
        if inviter.invitations > 0:
259
            if form.is_valid():
260
                try:
261
                    email = form.cleaned_data.get('username')
262
                    realname = form.cleaned_data.get('realname')
263
                    inviter.invite(email, realname)
264
                    message = _(astakos_messages.INVITATION_SENT) % locals()
265
                    messages.success(request, message)
266
                except SendMailError, e:
267
                    message = e.message
268
                    messages.error(request, message)
269
                    transaction.rollback()
270
                except BaseException, e:
271
                    message = _(astakos_messages.GENERIC_ERROR)
272
                    messages.error(request, message)
273
                    logger.exception(e)
274
                    transaction.rollback()
275
                else:
276
                    transaction.commit()
277
        else:
278
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
279
            messages.error(request, message)
280

    
281
    sent = [{'email': inv.username,
282
             'realname': inv.realname,
283
             'is_consumed': inv.is_consumed}
284
            for inv in request.user.invitations_sent.all()]
285
    kwargs = {'inviter': inviter,
286
              'sent': sent}
287
    context = get_context(request, extra_context, **kwargs)
288
    return render_response(template_name,
289
                           invitation_form=form,
290
                           context_instance=context)
291

    
292

    
293
@require_http_methods(["GET", "POST"])
294
@login_required
295
@signed_terms_required
296
def edit_profile(request, template_name='im/profile.html', extra_context=None):
297
    """
298
    Allows a user to edit his/her profile.
299

300
    In case of GET request renders a form for displaying the user information.
301
    In case of POST updates the user informantion and redirects to ``next``
302
    url parameter if exists.
303

304
    If the user isn't logged in, redirects to settings.LOGIN_URL.
305

306
    **Arguments**
307

308
    ``template_name``
309
        A custom template to use. This is optional; if not specified,
310
        this will default to ``im/profile.html``.
311

312
    ``extra_context``
313
        An dictionary of variables to add to the template context.
314

315
    **Template:**
316

317
    im/profile.html or ``template_name`` keyword argument.
318

319
    **Settings:**
320

321
    The view expectes the following settings are defined:
322

323
    * LOGIN_URL: login uri
324
    """
325
    extra_context = extra_context or {}
326
    form = ProfileForm(
327
        instance=request.user,
328
        session_key=request.session.session_key
329
    )
330
    extra_context['next'] = request.GET.get('next')
331
    if request.method == 'POST':
332
        form = ProfileForm(
333
            request.POST,
334
            instance=request.user,
335
            session_key=request.session.session_key
336
        )
337
        if form.is_valid():
338
            try:
339
                prev_token = request.user.auth_token
340
                user = form.save()
341
                form = ProfileForm(
342
                    instance=user,
343
                    session_key=request.session.session_key
344
                )
345
                next = restrict_next(
346
                    request.POST.get('next'),
347
                    domain=COOKIE_DOMAIN
348
                )
349
                if next:
350
                    return redirect(next)
351
                msg = _(astakos_messages.PROFILE_UPDATED)
352
                messages.success(request, msg)
353
            except ValueError, ve:
354
                messages.success(request, ve)
355
    elif request.method == "GET":
356
        request.user.is_verified = True
357
        request.user.save()
358

    
359
    # existing providers
360
    user_providers = request.user.get_active_auth_providers()
361

    
362
    # providers that user can add
363
    user_available_providers = request.user.get_available_auth_providers()
364

    
365
    return render_response(template_name,
366
                           profile_form = form,
367
                           user_providers = user_providers,
368
                           user_available_providers = user_available_providers,
369
                           context_instance = get_context(request,
370
                                                          extra_context))
371

    
372

    
373
@transaction.commit_manually
374
@require_http_methods(["GET", "POST"])
375
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
376
    """
377
    Allows a user to create a local account.
378

379
    In case of GET request renders a form for entering the user information.
380
    In case of POST handles the signup.
381

382
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
383
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
384
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
385
    (see activation_backends);
386

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

390
    On unsuccessful creation, renders ``template_name`` with an error message.
391

392
    **Arguments**
393

394
    ``template_name``
395
        A custom template to render. This is optional;
396
        if not specified, this will default to ``im/signup.html``.
397

398
    ``on_success``
399
        A custom template to render in case of success. This is optional;
400
        if not specified, this will default to ``im/signup_complete.html``.
401

402
    ``extra_context``
403
        An dictionary of variables to add to the template context.
404

405
    **Template:**
406

407
    im/signup.html or ``template_name`` keyword argument.
408
    im/signup_complete.html or ``on_success`` keyword argument.
409
    """
410
    extra_context = extra_context or {}
411
    if request.user.is_authenticated():
412
        return HttpResponseRedirect(reverse('edit_profile'))
413

    
414
    provider = get_query(request).get('provider', 'local')
415
    if not auth_providers.get_provider(provider).is_available_for_create():
416
        raise PermissionDenied
417

    
418
    id = get_query(request).get('id')
419
    try:
420
        instance = AstakosUser.objects.get(id=id) if id else None
421
    except AstakosUser.DoesNotExist:
422
        instance = None
423

    
424
    third_party_token = request.REQUEST.get('third_party_token', None)
425

    
426
    try:
427
        if not backend:
428
            backend = get_backend(request)
429
        form = backend.get_signup_form(provider, instance)
430
    except Exception, e:
431
        form = SimpleBackend(request).get_signup_form(provider)
432
        messages.error(request, e)
433
    if request.method == 'POST':
434
        if form.is_valid():
435
            user = form.save(commit=False)
436
            try:
437
                result = backend.handle_activation(user)
438
                status = messages.SUCCESS
439
                message = result.message
440

    
441
                form.store_user(user, request)
442

    
443
                if 'additional_email' in form.cleaned_data:
444
                    additional_email = form.cleaned_data['additional_email']
445
                    if additional_email != user.email:
446
                        user.additionalmail_set.create(email=additional_email)
447
                        msg = 'Additional email: %s saved for user %s.' % (
448
                            additional_email,
449
                            user.email
450
                        )
451
                        logger._log(LOGGING_LEVEL, msg, [])
452
                if user and user.is_active:
453
                    next = request.POST.get('next', '')
454
                    response = prepare_response(request, user, next=next)
455
                    transaction.commit()
456
                    return response
457
                messages.add_message(request, status, message)
458
                transaction.commit()
459
                return render_response(
460
                    on_success,
461
                    context_instance=get_context(
462
                        request,
463
                        extra_context
464
                    )
465
                )
466
            except SendMailError, e:
467
                logger.exception(e)
468
                status = messages.ERROR
469
                message = e.message
470
                messages.error(request, message)
471
                transaction.rollback()
472
            except BaseException, e:
473
                logger.exception(e)
474
                message = _(astakos_messages.GENERIC_ERROR)
475
                messages.error(request, message)
476
                logger.exception(e)
477
                transaction.rollback()
478
    return render_response(template_name,
479
                           signup_form=form,
480
                           third_party_token=third_party_token,
481
                           provider=provider,
482
                           context_instance=get_context(request, extra_context))
483

    
484

    
485
@require_http_methods(["GET", "POST"])
486
@login_required
487
@signed_terms_required
488
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
489
    """
490
    Allows a user to send feedback.
491

492
    In case of GET request renders a form for providing the feedback information.
493
    In case of POST sends an email to support team.
494

495
    If the user isn't logged in, redirects to settings.LOGIN_URL.
496

497
    **Arguments**
498

499
    ``template_name``
500
        A custom template to use. This is optional; if not specified,
501
        this will default to ``im/feedback.html``.
502

503
    ``extra_context``
504
        An dictionary of variables to add to the template context.
505

506
    **Template:**
507

508
    im/signup.html or ``template_name`` keyword argument.
509

510
    **Settings:**
511

512
    * LOGIN_URL: login uri
513
    """
514
    extra_context = extra_context or {}
515
    if request.method == 'GET':
516
        form = FeedbackForm()
517
    if request.method == 'POST':
518
        if not request.user:
519
            return HttpResponse('Unauthorized', status=401)
520

    
521
        form = FeedbackForm(request.POST)
522
        if form.is_valid():
523
            msg = form.cleaned_data['feedback_msg']
524
            data = form.cleaned_data['feedback_data']
525
            try:
526
                send_feedback(msg, data, request.user, email_template_name)
527
            except SendMailError, e:
528
                messages.error(request, message)
529
            else:
530
                message = _(astakos_messages.FEEDBACK_SENT)
531
                messages.success(request, message)
532
    return render_response(template_name,
533
                           feedback_form=form,
534
                           context_instance=get_context(request, extra_context))
535

    
536

    
537
@require_http_methods(["GET"])
538
@signed_terms_required
539
def logout(request, template='registration/logged_out.html', extra_context=None):
540
    """
541
    Wraps `django.contrib.auth.logout`.
542
    """
543
    extra_context = extra_context or {}
544
    response = HttpResponse()
545
    if request.user.is_authenticated():
546
        email = request.user.email
547
        auth_logout(request)
548
    next = restrict_next(
549
        request.GET.get('next'),
550
        domain=COOKIE_DOMAIN
551
    )
552
    if next:
553
        response['Location'] = next
554
        response.status_code = 302
555
    elif LOGOUT_NEXT:
556
        response['Location'] = LOGOUT_NEXT
557
        response.status_code = 301
558
    else:
559
        messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
560
        context = get_context(request, extra_context)
561
        response.write(render_to_string(template, context_instance=context))
562
    return response
563

    
564

    
565
@require_http_methods(["GET", "POST"])
566
@transaction.commit_manually
567
def activate(request, greeting_email_template_name='im/welcome_email.txt',
568
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
569
    """
570
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
571
    and renews the user token.
572

573
    The view uses commit_manually decorator in order to ensure the user state will be updated
574
    only if the email will be send successfully.
575
    """
576
    token = request.GET.get('auth')
577
    next = request.GET.get('next')
578
    try:
579
        user = AstakosUser.objects.get(auth_token=token)
580
    except AstakosUser.DoesNotExist:
581
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
582

    
583
    if user.is_active:
584
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
585
        messages.error(request, message)
586
        return index(request)
587

    
588
    try:
589
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
590
        response = prepare_response(request, user, next, renew=True)
591
        transaction.commit()
592
        return response
593
    except SendMailError, e:
594
        message = e.message
595
        messages.add_message(request, messages.ERROR, message)
596
        transaction.rollback()
597
        return index(request)
598
    except BaseException, e:
599
        status = messages.ERROR
600
        message = _(astakos_messages.GENERIC_ERROR)
601
        messages.add_message(request, messages.ERROR, message)
602
        logger.exception(e)
603
        transaction.rollback()
604
        return index(request)
605

    
606

    
607
@require_http_methods(["GET", "POST"])
608
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
609
    extra_context = extra_context or {}
610
    term = None
611
    terms = None
612
    if not term_id:
613
        try:
614
            term = ApprovalTerms.objects.order_by('-id')[0]
615
        except IndexError:
616
            pass
617
    else:
618
        try:
619
            term = ApprovalTerms.objects.get(id=term_id)
620
        except ApprovalTerms.DoesNotExist, e:
621
            pass
622

    
623
    if not term:
624
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
625
        return HttpResponseRedirect(reverse('index'))
626
    f = open(term.location, 'r')
627
    terms = f.read()
628

    
629
    if request.method == 'POST':
630
        next = restrict_next(
631
            request.POST.get('next'),
632
            domain=COOKIE_DOMAIN
633
        )
634
        if not next:
635
            next = reverse('index')
636
        form = SignApprovalTermsForm(request.POST, instance=request.user)
637
        if not form.is_valid():
638
            return render_response(template_name,
639
                                   terms=terms,
640
                                   approval_terms_form=form,
641
                                   context_instance=get_context(request, extra_context))
642
        user = form.save()
643
        return HttpResponseRedirect(next)
644
    else:
645
        form = None
646
        if request.user.is_authenticated() and not request.user.signed_terms:
647
            form = SignApprovalTermsForm(instance=request.user)
648
        return render_response(template_name,
649
                               terms=terms,
650
                               approval_terms_form=form,
651
                               context_instance=get_context(request, extra_context))
652

    
653

    
654
@require_http_methods(["GET", "POST"])
655
@login_required
656
@signed_terms_required
657
@transaction.commit_manually
658
def change_email(request, activation_key=None,
659
                 email_template_name='registration/email_change_email.txt',
660
                 form_template_name='registration/email_change_form.html',
661
                 confirm_template_name='registration/email_change_done.html',
662
                 extra_context=None):
663
    extra_context = extra_context or {}
664
    if activation_key:
665
        try:
666
            user = EmailChange.objects.change_email(activation_key)
667
            if request.user.is_authenticated() and request.user == user:
668
                msg = _(astakos_messages.EMAIL_CHANGED)
669
                messages.success(request, msg)
670
                auth_logout(request)
671
                response = prepare_response(request, user)
672
                transaction.commit()
673
                return response
674
        except ValueError, e:
675
            messages.error(request, e)
676
        return render_response(confirm_template_name,
677
                               modified_user=user if 'user' in locals(
678
                               ) else None,
679
                               context_instance=get_context(request,
680
                                                            extra_context))
681

    
682
    if not request.user.is_authenticated():
683
        path = quote(request.get_full_path())
684
        url = request.build_absolute_uri(reverse('index'))
685
        return HttpResponseRedirect(url + '?next=' + path)
686
    form = EmailChangeForm(request.POST or None)
687
    if request.method == 'POST' and form.is_valid():
688
        try:
689
            ec = form.save(email_template_name, request)
690
        except SendMailError, e:
691
            msg = e
692
            messages.error(request, msg)
693
            transaction.rollback()
694
        except IntegrityError, e:
695
            msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
696
            messages.error(request, msg)
697
        else:
698
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
699
            messages.success(request, msg)
700
            transaction.commit()
701
    return render_response(
702
        form_template_name,
703
        form=form,
704
        context_instance=get_context(request, extra_context)
705
    )
706

    
707

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

    
710
    if settings.MODERATION_ENABLED:
711
        raise PermissionDenied
712

    
713
    extra_context = extra_context or {}
714
    try:
715
        u = AstakosUser.objects.get(id=user_id)
716
    except AstakosUser.DoesNotExist:
717
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
718
    else:
719
        try:
720
            send_activation_func(u)
721
            msg = _(astakos_messages.ACTIVATION_SENT)
722
            messages.success(request, msg)
723
        except SendMailError, e:
724
            messages.error(request, e)
725
    return render_response(
726
        template_name,
727
        login_form = LoginForm(request=request),
728
        context_instance = get_context(
729
            request,
730
            extra_context
731
        )
732
    )
733

    
734
class ResourcePresentation():
735

    
736
    def __init__(self, data):
737
        self.data = data
738

    
739
    def update_from_result(self, result):
740
        if result.is_success:
741
            for r in result.data:
742
                rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
743
                if not rname in self.data['resources']:
744
                    self.data['resources'][rname] = {}
745

    
746
                self.data['resources'][rname].update(r)
747
                self.data['resources'][rname]['id'] = rname
748
                group = r.get('group')
749
                if not group in self.data['groups']:
750
                    self.data['groups'][group] = {}
751

    
752
                self.data['groups'][r.get('group')].update({'name': r.get('group')})
753

    
754
    def test(self, quota_dict):
755
        for k, v in quota_dict.iteritems():
756
            rname = k
757
            value = v
758
            if not rname in self.data['resources']:
759
                self.data['resources'][rname] = {}
760

    
761

    
762
            self.data['resources'][rname]['value'] = value
763

    
764

    
765
    def update_from_result_report(self, result):
766
        if result.is_success:
767
            for r in result.data:
768
                rname = r.get('name')
769
                if not rname in self.data['resources']:
770
                    self.data['resources'][rname] = {}
771

    
772
                self.data['resources'][rname].update(r)
773
                self.data['resources'][rname]['id'] = rname
774
                group = r.get('group')
775
                if not group in self.data['groups']:
776
                    self.data['groups'][group] = {}
777

    
778
                self.data['groups'][r.get('group')].update({'name': r.get('group')})
779

    
780
    def get_group_resources(self, group):
781
        return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
782

    
783
    def get_groups_resources(self):
784
        for g in self.data['groups']:
785
            yield g, self.get_group_resources(g)
786

    
787
    def get_quota(self, group_quotas):
788
        for r, v in group_quotas:
789
            rname = str(r)
790
            quota = self.data['resources'].get(rname)
791
            quota['value'] = v
792
            yield quota
793

    
794

    
795
    def get_policies(self, policies_data):
796
        for policy in policies_data:
797
            rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
798
            policy.update(self.data['resources'].get(rname))
799
            yield policy
800

    
801
    def __repr__(self):
802
        return self.data.__repr__()
803

    
804
    def __iter__(self, *args, **kwargs):
805
        return self.data.__iter__(*args, **kwargs)
806

    
807
    def __getitem__(self, *args, **kwargs):
808
        return self.data.__getitem__(*args, **kwargs)
809

    
810
    def get(self, *args, **kwargs):
811
        return self.data.get(*args, **kwargs)
812

    
813

    
814

    
815
@require_http_methods(["GET", "POST"])
816
@signed_terms_required
817
@login_required
818
def group_add(request, kind_name='default'):
819
    result = callpoint.list_resources()
820
    resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
821
    resource_catalog.update_from_result(result)
822

    
823
    if not result.is_success:
824
        messages.error(
825
            request,
826
            'Unable to retrieve system resources: %s' % result.reason
827
    )
828

    
829
    try:
830
        kind = GroupKind.objects.get(name=kind_name)
831
    except:
832
        return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
833

    
834

    
835

    
836
    post_save_redirect = '/im/group/%(id)s/'
837
    context_processors = None
838
    model, form_class = get_model_and_form_class(
839
        model=None,
840
        form_class=AstakosGroupCreationForm
841
    )
842

    
843
    if request.method == 'POST':
844
        form = form_class(request.POST, request.FILES)
845
        if form.is_valid():
846
            policies = form.policies()
847
            return render_response(
848
                template='im/astakosgroup_form_summary.html',
849
                context_instance=get_context(request),
850
                form=AstakosGroupCreationSummaryForm(form.cleaned_data),
851
                policies=resource_catalog.get_policies(policies)
852
            )
853
    else:
854
        now = datetime.now()
855
        data = {
856
            'kind': kind,
857
        }
858
        for group, resources in resource_catalog.get_groups_resources():
859
            data['is_selected_%s' % group] = False
860
            for resource in resources:
861
                data['%s_uplimit' % resource] = ''
862

    
863
        form = form_class(data)
864

    
865
    # Create the template, context, response
866
    template_name = "%s/%s_form.html" % (
867
        model._meta.app_label,
868
        model._meta.object_name.lower()
869
    )
870
    t = template_loader.get_template(template_name)
871
    c = RequestContext(request, {
872
        'form': form,
873
        'kind': kind,
874
        'resource_catalog':resource_catalog,
875
    }, context_processors)
876
    return HttpResponse(t.render(c))
877

    
878

    
879
#@require_hsttp_methods(["POST"])
880
@require_http_methods(["GET", "POST"])
881
@signed_terms_required
882
@login_required
883
@transaction.commit_manually
884
def group_add_complete(request):
885
    model = AstakosGroup
886
    form = AstakosGroupCreationSummaryForm(request.POST)
887
    if form.is_valid():
888
        d = form.cleaned_data
889
        d['owners'] = [request.user]
890
        result = callpoint.create_groups((d,)).next()
891
        if result.is_success:
892
            new_object = result.data[0]
893
            # send notification
894
            try:
895
                send_group_creation_notification(
896
                    template_name='im/group_creation_notification.txt',
897
                    dictionary={
898
                        'group': new_object,
899
                        'owner': request.user,
900
                        'policies': d.get('policies', [])
901
                    }
902
                )
903
            except SendNotificationError, e:
904
                messages.error(request, e, fail_silently=True)
905
                transaction.rollback()
906
            else:
907
                msg = _(astakos_messages.OBJECT_CREATED) %\
908
                    {"verbose_name": model._meta.verbose_name}
909
                message = _(astakos_messages.NOTIFICATION_SENT) % 'a project'
910
                messages.success(request, msg, fail_silently=True)
911
                transaction.commit()
912
        else:
913
            d = {"verbose_name": model._meta.verbose_name,
914
                 "reason":result.reason}
915
            msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
916
            messages.error(request, msg, fail_silently=True)
917
    return render_response(
918
        template='im/astakosgroup_form_summary.html',
919
        context_instance=get_context(request),
920
        form=form,
921
        policies=form.cleaned_data.get('policies')
922
    )
923

    
924

    
925
#@require_http_methods(["GET"])
926
@require_http_methods(["GET", "POST"])
927
@signed_terms_required
928
@login_required
929
def group_list(request):
930
    none = request.user.astakos_groups.none()
931
    query = """
932
        SELECT auth_group.id,
933
        auth_group.name AS groupname,
934
        im_groupkind.name AS kindname,
935
        im_astakosgroup.*,
936
        owner.email AS groupowner,
937
        (SELECT COUNT(*) FROM im_membership
938
            WHERE group_id = im_astakosgroup.group_ptr_id
939
            AND date_joined IS NOT NULL) AS approved_members_num,
940
        (SELECT CASE WHEN(
941
                    SELECT date_joined FROM im_membership
942
                    WHERE group_id = im_astakosgroup.group_ptr_id
943
                    AND person_id = %(id)s) IS NULL
944
                    THEN 0 ELSE 1 END) AS membership_status
945
        FROM im_astakosgroup
946
        INNER JOIN im_membership ON (
947
            im_astakosgroup.group_ptr_id = im_membership.group_id)
948
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
949
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
950
        LEFT JOIN im_astakosuser_owner ON (
951
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
952
        LEFT JOIN auth_user as owner ON (
953
            im_astakosuser_owner.astakosuser_id = owner.id)
954
        WHERE im_membership.person_id = %(id)s
955
        AND im_groupkind.name != 'default'
956
        """ % request.user.__dict__
957

    
958
    # validate sorting
959
    sorting = 'groupname'
960
    sort_form = AstakosGroupSortForm(request.GET)
961
    if sort_form.is_valid():
962
        sorting = sort_form.cleaned_data.get('sorting')
963
    query = query+" ORDER BY %s ASC" %sorting
964
    
965
    q = AstakosGroup.objects.raw(query)
966
    
967
    # Create the template, context, response
968
    template_name = "%s/%s_list.html" % (
969
        q.model._meta.app_label,
970
        q.model._meta.object_name.lower()
971
    )
972
    extra_context = dict(
973
        is_search=False,
974
        q=q,
975
        sorting=sorting,
976
        page=request.GET.get('page', 1)
977
    )
978
    return render_response(template_name,
979
                           context_instance=get_context(request, extra_context)
980
    )
981

    
982

    
983
@require_http_methods(["GET", "POST"])
984
@signed_terms_required
985
@login_required
986
def group_detail(request, group_id):
987
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
988
    q = q.extra(select={
989
        'is_member': """SELECT CASE WHEN EXISTS(
990
                            SELECT id FROM im_membership
991
                            WHERE group_id = im_astakosgroup.group_ptr_id
992
                            AND person_id = %s)
993
                        THEN 1 ELSE 0 END""" % request.user.id,
994
        'is_owner': """SELECT CASE WHEN EXISTS(
995
                        SELECT id FROM im_astakosuser_owner
996
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
997
                        AND astakosuser_id = %s)
998
                        THEN 1 ELSE 0 END""" % request.user.id,
999
        'is_active_member': """SELECT CASE WHEN(
1000
                        SELECT date_joined FROM im_membership
1001
                        WHERE group_id = im_astakosgroup.group_ptr_id
1002
                        AND person_id = %s) IS NULL
1003
                        THEN 0 ELSE 1 END""" % request.user.id,
1004
        'kindname': """SELECT name FROM im_groupkind
1005
                       WHERE id = im_astakosgroup.kind_id"""})
1006

    
1007
    model = q.model
1008
    context_processors = None
1009
    mimetype = None
1010
    try:
1011
        obj = q.get()
1012
    except AstakosGroup.DoesNotExist:
1013
        raise Http404("No %s found matching the query" % (
1014
            model._meta.verbose_name))
1015

    
1016
    update_form = AstakosGroupUpdateForm(instance=obj)
1017
    addmembers_form = AddGroupMembersForm()
1018
    if request.method == 'POST':
1019
        update_data = {}
1020
        addmembers_data = {}
1021
        for k, v in request.POST.iteritems():
1022
            if k in update_form.fields:
1023
                update_data[k] = v
1024
            if k in addmembers_form.fields:
1025
                addmembers_data[k] = v
1026
        update_data = update_data or None
1027
        addmembers_data = addmembers_data or None
1028
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1029
        addmembers_form = AddGroupMembersForm(addmembers_data)
1030
        if update_form.is_valid():
1031
            update_form.save()
1032
        if addmembers_form.is_valid():
1033
            try:
1034
                map(obj.approve_member, addmembers_form.valid_users)
1035
            except AssertionError:
1036
                msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1037
                messages.error(request, msg)
1038
            addmembers_form = AddGroupMembersForm()
1039

    
1040
    template_name = "%s/%s_detail.html" % (
1041
        model._meta.app_label, model._meta.object_name.lower())
1042
    t = template_loader.get_template(template_name)
1043
    c = RequestContext(request, {
1044
        'object': obj,
1045
    }, context_processors)
1046

    
1047
    # validate sorting
1048
    sorting = 'person__email'
1049
    form = MembersSortForm(request.GET)
1050
    if form.is_valid():
1051
        sorting = form.cleaned_data.get('sorting')
1052
    
1053
    result = callpoint.list_resources()
1054
    resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1055
    resource_catalog.update_from_result(result)
1056

    
1057

    
1058
    if not result.is_success:
1059
        messages.error(
1060
            request,
1061
            'Unable to retrieve system resources: %s' % result.reason
1062
    )
1063

    
1064
    extra_context = {'update_form': update_form,
1065
                     'addmembers_form': addmembers_form,
1066
                     'page': request.GET.get('page', 1),
1067
                     'sorting': sorting,
1068
                     'resource_catalog':resource_catalog,
1069
                     'quota':resource_catalog.get_quota(obj.quota)}
1070
    for key, value in extra_context.items():
1071
        if callable(value):
1072
            c[key] = value()
1073
        else:
1074
            c[key] = value
1075
    response = HttpResponse(t.render(c), mimetype=mimetype)
1076
    populate_xheaders(
1077
        request, response, model, getattr(obj, obj._meta.pk.name))
1078
    return response
1079

    
1080

    
1081
@require_http_methods(["GET", "POST"])
1082
@signed_terms_required
1083
@login_required
1084
def group_search(request, extra_context=None, **kwargs):
1085
    q = request.GET.get('q')
1086
    if request.method == 'GET':
1087
        form = AstakosGroupSearchForm({'q': q} if q else None)
1088
    else:
1089
        form = AstakosGroupSearchForm(get_query(request))
1090
        if form.is_valid():
1091
            q = form.cleaned_data['q'].strip()
1092
    
1093
    sorting = 'groupname'
1094
    if q:
1095
        queryset = AstakosGroup.objects.select_related()
1096
        queryset = queryset.filter(~Q(kind__name='default'))
1097
        queryset = queryset.filter(name__contains=q)
1098
        queryset = queryset.filter(approval_date__isnull=False)
1099
        queryset = queryset.extra(select={
1100
                                  'groupname': "auth_group.name",
1101
                                  'kindname': "im_groupkind.name",
1102
                                  'approved_members_num': """
1103
                    SELECT COUNT(*) FROM im_membership
1104
                    WHERE group_id = im_astakosgroup.group_ptr_id
1105
                    AND date_joined IS NOT NULL""",
1106
                                  'membership_approval_date': """
1107
                    SELECT date_joined FROM im_membership
1108
                    WHERE group_id = im_astakosgroup.group_ptr_id
1109
                    AND person_id = %s""" % request.user.id,
1110
                                  'is_member': """
1111
                    SELECT CASE WHEN EXISTS(
1112
                    SELECT date_joined FROM im_membership
1113
                    WHERE group_id = im_astakosgroup.group_ptr_id
1114
                    AND person_id = %s)
1115
                    THEN 1 ELSE 0 END""" % request.user.id,
1116
                                  'is_owner': """
1117
                    SELECT CASE WHEN EXISTS(
1118
                    SELECT id FROM im_astakosuser_owner
1119
                    WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1120
                    AND astakosuser_id = %s)
1121
                    THEN 1 ELSE 0 END""" % request.user.id,
1122
                    'is_owner': """SELECT CASE WHEN EXISTS(
1123
                        SELECT id FROM im_astakosuser_owner
1124
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1125
                        AND astakosuser_id = %s)
1126
                        THEN 1 ELSE 0 END""" % request.user.id,
1127
                    })
1128
        
1129
        # validate sorting
1130
        sort_form = AstakosGroupSortForm(request.GET)
1131
        if sort_form.is_valid():
1132
            sorting = sort_form.cleaned_data.get('sorting')
1133
        queryset = queryset.order_by(sorting)
1134

    
1135
    else:
1136
        queryset = AstakosGroup.objects.none()
1137
    return object_list(
1138
        request,
1139
        queryset,
1140
        paginate_by=PAGINATE_BY_ALL,
1141
        page=request.GET.get('page') or 1,
1142
        template_name='im/astakosgroup_list.html',
1143
        extra_context=dict(form=form,
1144
                           is_search=True,
1145
                           q=q,
1146
                           sorting=sorting))
1147

    
1148

    
1149
@require_http_methods(["GET", "POST"])
1150
@signed_terms_required
1151
@login_required
1152
def group_all(request, extra_context=None, **kwargs):
1153
    q = AstakosGroup.objects.select_related()
1154
    q = q.filter(~Q(kind__name='default'))
1155
    q = q.filter(approval_date__isnull=False)
1156
    q = q.extra(select={
1157
                'groupname': "auth_group.name",
1158
                'kindname': "im_groupkind.name",
1159
                'approved_members_num': """
1160
                    SELECT COUNT(*) FROM im_membership
1161
                    WHERE group_id = im_astakosgroup.group_ptr_id
1162
                    AND date_joined IS NOT NULL""",
1163
                'membership_approval_date': """
1164
                    SELECT date_joined FROM im_membership
1165
                    WHERE group_id = im_astakosgroup.group_ptr_id
1166
                    AND person_id = %s""" % request.user.id,
1167
                'is_member': """
1168
                    SELECT CASE WHEN EXISTS(
1169
                    SELECT date_joined FROM im_membership
1170
                    WHERE group_id = im_astakosgroup.group_ptr_id
1171
                    AND person_id = %s)
1172
                    THEN 1 ELSE 0 END""" % request.user.id,
1173
                 'is_owner': """SELECT CASE WHEN EXISTS(
1174
                        SELECT id FROM im_astakosuser_owner
1175
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1176
                        AND astakosuser_id = %s)
1177
                        THEN 1 ELSE 0 END""" % request.user.id,   })
1178
    
1179
    # validate sorting
1180
    sorting = 'groupname'
1181
    sort_form = AstakosGroupSortForm(request.GET)
1182
    if sort_form.is_valid():
1183
        sorting = sort_form.cleaned_data.get('sorting')
1184
    q = q.order_by(sorting)
1185
    
1186
    return object_list(
1187
        request,
1188
        q,
1189
        paginate_by=PAGINATE_BY_ALL,
1190
        page=request.GET.get('page') or 1,
1191
        template_name='im/astakosgroup_list.html',
1192
        extra_context=dict(form=AstakosGroupSearchForm(),
1193
                           is_search=True,
1194
                           sorting=sorting))
1195

    
1196

    
1197
#@require_http_methods(["POST"])
1198
@require_http_methods(["POST", "GET"])
1199
@signed_terms_required
1200
@login_required
1201
def group_join(request, group_id):
1202
    m = Membership(group_id=group_id,
1203
                   person=request.user,
1204
                   date_requested=datetime.now())
1205
    try:
1206
        m.save()
1207
        post_save_redirect = reverse(
1208
            'group_detail',
1209
            kwargs=dict(group_id=group_id))
1210
        return HttpResponseRedirect(post_save_redirect)
1211
    except IntegrityError, e:
1212
        logger.exception(e)
1213
        msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1214
        messages.error(request, msg)
1215
        return group_search(request)
1216

    
1217

    
1218
@require_http_methods(["POST"])
1219
@signed_terms_required
1220
@login_required
1221
def group_leave(request, group_id):
1222
    try:
1223
        m = Membership.objects.select_related().get(
1224
            group__id=group_id,
1225
            person=request.user)
1226
    except Membership.DoesNotExist:
1227
        return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1228
    if request.user in m.group.owner.all():
1229
        return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1230
    return delete_object(
1231
        request,
1232
        model=Membership,
1233
        object_id=m.id,
1234
        template_name='im/astakosgroup_list.html',
1235
        post_delete_redirect=reverse(
1236
            'group_detail',
1237
            kwargs=dict(group_id=group_id)))
1238

    
1239

    
1240
def handle_membership(func):
1241
    @wraps(func)
1242
    def wrapper(request, group_id, user_id):
1243
        try:
1244
            m = Membership.objects.select_related().get(
1245
                group__id=group_id,
1246
                person__id=user_id)
1247
        except Membership.DoesNotExist:
1248
            return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1249
        else:
1250
            if request.user not in m.group.owner.all():
1251
                return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1252
            func(request, m)
1253
            return group_detail(request, group_id)
1254
    return wrapper
1255

    
1256

    
1257
#@require_http_methods(["POST"])
1258
@require_http_methods(["POST", "GET"])
1259
@signed_terms_required
1260
@login_required
1261
@handle_membership
1262
def approve_member(request, membership):
1263
    try:
1264
        membership.approve()
1265
        realname = membership.person.realname
1266
        msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1267
        messages.success(request, msg)
1268
    except AssertionError:
1269
        msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1270
        messages.error(request, msg)
1271
    except BaseException, e:
1272
        logger.exception(e)
1273
        realname = membership.person.realname
1274
        msg = _(astakos_messages.GENERIC_ERROR)
1275
        messages.error(request, msg)
1276

    
1277

    
1278
@signed_terms_required
1279
@login_required
1280
@handle_membership
1281
def disapprove_member(request, membership):
1282
    try:
1283
        membership.disapprove()
1284
        realname = membership.person.realname
1285
        msg = astakos_messages.MEMBER_REMOVED % locals()
1286
        messages.success(request, msg)
1287
    except BaseException, e:
1288
        logger.exception(e)
1289
        msg = _(astakos_messages.GENERIC_ERROR)
1290
        messages.error(request, msg)
1291

    
1292

    
1293
#@require_http_methods(["GET"])
1294
@require_http_methods(["POST", "GET"])
1295
@signed_terms_required
1296
@login_required
1297
def resource_usage(request):
1298
    def with_class(entry):
1299
        entry['load_class'] = 'red'
1300
        max_value = float(entry['maxValue'])
1301
        curr_value = float(entry['currValue'])
1302
        entry['ratio_limited']= 0
1303
        if max_value > 0 :
1304
            entry['ratio'] = (curr_value / max_value) * 100
1305
        else:
1306
            entry['ratio'] = 0
1307
        if entry['ratio'] < 66:
1308
            entry['load_class'] = 'yellow'
1309
        if entry['ratio'] < 33:
1310
            entry['load_class'] = 'green'
1311
        if entry['ratio']<0:
1312
            entry['ratio'] = 0
1313
        if entry['ratio']>100:
1314
            entry['ratio_limited'] = 100
1315
        else:
1316
            entry['ratio_limited'] = entry['ratio']
1317
        
1318
        return entry
1319

    
1320
    def pluralize(entry):
1321
        entry['plural'] = engine.plural(entry.get('name'))
1322
        return entry
1323

    
1324
    result = callpoint.get_user_usage(request.user.id)
1325
    if result.is_success:
1326
        backenddata = map(with_class, result.data)
1327
        data = map(pluralize, result.data)
1328
    else:
1329
        data = None
1330
        messages.error(request, result.reason)
1331
    resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1332
    resource_catalog.update_from_result_report(result)
1333
    return render_response('im/resource_usage.html',
1334
                           data=data,
1335
                           context_instance=get_context(request),
1336
                           resource_catalog=resource_catalog,
1337
                           result=result)
1338

    
1339

    
1340
def group_create_list(request):
1341
    form = PickResourceForm()
1342
    return render_response(
1343
        template='im/astakosgroup_create_list.html',
1344
        context_instance=get_context(request),)
1345

    
1346

    
1347
##@require_http_methods(["GET"])
1348
#@require_http_methods(["POST", "GET"])
1349
#@signed_terms_required
1350
#@login_required
1351
#def billing(request):
1352
#
1353
#    today = datetime.today()
1354
#    month_last_day = calendar.monthrange(today.year, today.month)[1]
1355
#    start = request.POST.get('datefrom', None)
1356
#    if start:
1357
#        today = datetime.fromtimestamp(int(start))
1358
#        month_last_day = calendar.monthrange(today.year, today.month)[1]
1359
#
1360
#    start = datetime(today.year, today.month, 1).strftime("%s")
1361
#    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1362
#    r = request_billing.apply(args=('pgerakios@grnet.gr',
1363
#                                    int(start) * 1000,
1364
#                                    int(end) * 1000))
1365
#    data = {}
1366
#
1367
#    try:
1368
#        status, data = r.result
1369
#        data = _clear_billing_data(data)
1370
#        if status != 200:
1371
#            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1372
#    except:
1373
#        messages.error(request, r.result)
1374
#
1375
#    return render_response(
1376
#        template='im/billing.html',
1377
#        context_instance=get_context(request),
1378
#        data=data,
1379
#        zerodate=datetime(month=1, year=1970, day=1),
1380
#        today=today,
1381
#        start=int(start),
1382
#        month_last_day=month_last_day)
1383

    
1384

    
1385
#def _clear_billing_data(data):
1386
#
1387
#    # remove addcredits entries
1388
#    def isnotcredit(e):
1389
#        return e['serviceName'] != "addcredits"
1390
#
1391
#    # separate services
1392
#    def servicefilter(service_name):
1393
#        service = service_name
1394
#
1395
#        def fltr(e):
1396
#            return e['serviceName'] == service
1397
#        return fltr
1398
#
1399
#    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1400
#    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1401
#    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1402
#    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1403
#
1404
#    return data
1405

    
1406

    
1407
# #@require_http_methods(["GET"])
1408
# @require_http_methods(["POST", "GET"])
1409
# @signed_terms_required
1410
# @login_required
1411
# def timeline(request):
1412
# #    data = {'entity':request.user.email}
1413
#     timeline_body = ()
1414
#     timeline_header = ()
1415
# #    form = TimelineForm(data)
1416
#     form = TimelineForm()
1417
#     if request.method == 'POST':
1418
#         data = request.POST
1419
#         form = TimelineForm(data)
1420
#         if form.is_valid():
1421
#             data = form.cleaned_data
1422
#             timeline_header = ('entity', 'resource',
1423
#                                'event name', 'event date',
1424
#                                'incremental cost', 'total cost')
1425
#             timeline_body = timeline_charge(
1426
#                 data['entity'], data['resource'],
1427
#                 data['start_date'], data['end_date'],
1428
#                 data['details'], data['operation'])
1429
# 
1430
#     return render_response(template='im/timeline.html',
1431
#                            context_instance=get_context(request),
1432
#                            form=form,
1433
#                            timeline_header=timeline_header,
1434
#                            timeline_body=timeline_body)
1435
#     return data
1436

    
1437

    
1438
# TODO: action only on POST and user should confirm the removal
1439
@require_http_methods(["GET", "POST"])
1440
@login_required
1441
@signed_terms_required
1442
def remove_auth_provider(request, pk):
1443
    try:
1444
        provider = request.user.auth_providers.get(pk=pk)
1445
    except AstakosUserAuthProvider.DoesNotExist:
1446
        raise Http404
1447

    
1448
    if provider.can_remove():
1449
        provider.delete()
1450
        return HttpResponseRedirect(reverse('edit_profile'))
1451
    else:
1452
        raise PermissionDenied
1453

    
1454

    
1455
def how_it_works(request):
1456
    return render_response(
1457
        'im/how_it_works.html',
1458
        context_instance=get_context(request))
1459

    
1460
@require_http_methods(["GET", "POST"])
1461
@signed_terms_required
1462
@login_required
1463
@transaction.commit_manually
1464
def project_add(request):
1465
    rollback = False
1466
    result = callpoint.list_resources()
1467
    resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1468
    resource_catalog.update_from_result(result)
1469

    
1470
    if not result.is_success:
1471
        messages.error(
1472
            request,
1473
            'Unable to retrieve system resources: %s' % result.reason
1474
    )
1475
    extra_context = {'resource_catalog':resource_catalog}
1476

    
1477
    try:
1478
        r = create_object(request, template_name='im/projects/projectapplication_form.html',
1479
            extra_context=extra_context, post_save_redirect='/im/project/list/',
1480
            form_class=ProjectApplicationForm)
1481
        return r
1482
    except NotificationError, e:
1483
        rollback = True
1484
        messages.error(request, e.message)
1485
        return render_response(
1486
            'im/projects/projectapplication_form.html',
1487
            sorting = 'definition__name',
1488
            form = ProjectApplicationForm(),
1489
            context_instance=get_context(request, extra_context)
1490
        )
1491
    finally:
1492
        if rollback:
1493
            transaction.rollback()
1494
        else:
1495
            transaction.commit()
1496

    
1497

    
1498
@require_http_methods(["GET"])
1499
@signed_terms_required
1500
@login_required
1501
def project_list(request):
1502
    q = ProjectApplication.objects.filter(owner=request.user)
1503
    q |= ProjectApplication.objects.filter(
1504
        project__in=request.user.projectmembership_set.values_list('project', flat=True)
1505
    )
1506
    q = q.select_related()
1507
    sorting = 'definition__name'
1508
    sort_form = ProjectSortForm(request.GET)
1509
    if sort_form.is_valid():
1510
        sorting = sort_form.cleaned_data.get('sorting')
1511
    q = q.order_by(sorting)
1512
    
1513
    return object_list(
1514
        request,
1515
        q,
1516
        paginate_by=PAGINATE_BY_ALL,
1517
        page=request.GET.get('page') or 1,
1518
        template_name='im/projects/project_list.html',
1519
        extra_context={
1520
            'is_search':False,
1521
            'sorting':sorting
1522
        }
1523
    )
1524

    
1525
@require_http_methods(["GET", "POST"])
1526
@signed_terms_required
1527
@login_required
1528
def project_update(request, id):
1529
    result = callpoint.list_resources()
1530
    resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1531
    resource_catalog.update_from_result(result)
1532

    
1533
    if not result.is_success:
1534
        messages.error(
1535
            request,
1536
            'Unable to retrieve system resources: %s' % result.reason
1537
    )
1538
    extra_context = {'resource_catalog':resource_catalog}
1539
    return update_object(
1540
        request,
1541
        slug=id,
1542
        slug_field='projectapplication__id',
1543
        template_name='im/projects/projectapplication_form.html',
1544
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1545
        form_class=ProjectApplicationForm)
1546
   
1547
# @require_http_methods(["GET", "POST"])
1548
# @signed_terms_required
1549
# @login_required
1550
# def project_application_detail(request, id):
1551
#     return object_detail(
1552
#         request,
1553
#         queryset=ProjectApplication.objects.select_related(),
1554
#         object_id=id,
1555
#         template_name='im/projects/projectapplication_detail.html'
1556
#     )
1557

    
1558
@require_http_methods(["GET", "POST"])
1559
@signed_terms_required
1560
@login_required
1561
def project_detail(request, id):
1562
    result = callpoint.list_resources()
1563
    resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1564
    resource_catalog.update_from_result(result)
1565

    
1566
    addmembers_form = AddProjectMembersForm()
1567
    if request.method == 'POST':
1568
        addmembers_form = AddProjectMembersForm(request.POST)
1569
        if addmembers_form.is_valid():
1570
            try:
1571
                obj = ProjectApplication.objects.get(id=id)
1572
                map(obj.project.accept_member, addmembers_form.valid_users)
1573
            except ProjectApplication.DoesNotExist, e:
1574
                messages.error(request, _(astakos_messages.UNKNOWN_IDENTIFIER))
1575
            except BaseException, e:
1576
                messages.error(request, e)
1577
            addmembers_form = AddProjectMembersForm()
1578
    
1579
    # validate sorting
1580
    sorting = 'person__email'
1581
    form = MembersSortForm(request.GET or request.POST)
1582
    if form.is_valid():
1583
        sorting = form.cleaned_data.get('sorting')
1584

    
1585
    return object_detail(
1586
        request,
1587
        queryset=ProjectApplication.objects.select_related(),
1588
        object_id=id,
1589
        template_name='im/projects/project_detail.html',
1590
        extra_context={
1591
            'resource_catalog':resource_catalog,
1592
            'sorting':sorting,
1593
            'addmembers_form':addmembers_form
1594
        }
1595
    )
1596

    
1597
@require_http_methods(["GET", "POST"])
1598
@signed_terms_required
1599
@login_required
1600
def project_search(request):
1601
    queryset = ProjectApplication.objects.none()
1602
    if request.method == 'GET':
1603
        form = AstakosGroupSearchForm()
1604
    else:
1605
        form = AstakosGroupSearchForm(request.POST.get('q'))
1606
        if form.is_valid():
1607
            q = form.cleaned_data['q'].strip()
1608
            queryset = filter(~Q(project__last_approval_date__isnull=True))
1609
            queryset = queryset.filter(name__contains=q)
1610
    sorting = 'definition__name'        
1611
    # validate sorting
1612
    sort_form = AstakosGroupSortForm(request.GET)
1613
    if sort_form.is_valid():
1614
        sorting = sort_form.cleaned_data.get('sorting')
1615
    queryset = queryset.order_by(sorting)
1616
    return object_list(
1617
        request,
1618
        queryset,
1619
        paginate_by=PAGINATE_BY_ALL,
1620
        page=request.GET.get('page') or 1,
1621
        template_name='im/astakosgroup_list.html',
1622
        extra_context=dict(
1623
            form=form,
1624
            is_search=True,
1625
            sorting=sorting
1626
        )
1627
    )
1628

    
1629

    
1630
@require_http_methods(["GET"])
1631
@signed_terms_required
1632
@login_required
1633
def project_all(request):
1634
    q = ProjectApplication.objects.filter(~Q(project__last_approval_date__isnull=True))
1635
    q = q.select_related()
1636
    
1637
    sorting = 'definition__name'
1638
    sort_form = ProjectSortForm(request.GET)
1639
    if sort_form.is_valid():
1640
        sorting = sort_form.cleaned_data.get('sorting')
1641
    q = q.order_by(sorting)
1642
    
1643
    return object_list(
1644
        request,
1645
        q,
1646
        paginate_by=PAGINATE_BY_ALL,
1647
        page=request.GET.get('page') or 1,
1648
        template_name='im/projects/project_list.html',
1649
        extra_context={
1650
            'form':ProjectGroupSearchForm(),
1651
            'is_search':True,
1652
            'sorting':sorting
1653
        }
1654
    )
1655

    
1656
@require_http_methods(["GET", "POST"])
1657
@signed_terms_required
1658
@login_required
1659
@transaction.commit_manually
1660
def project_join(request, id):
1661
    rollback = False
1662
    try:
1663
        project = Project.objects.get(application__id=id)
1664
        m = ProjectMembership(
1665
            project=project,
1666
            person=request.user,
1667
            request_date=datetime.now())
1668
        m.save()
1669
    except Project.DoesNotExist, e:
1670
        msg = _(astakos_messages.UNKNOWN_IDENTIFIER)
1671
        messages.error(request, msg)
1672
    except IntegrityError, e:
1673
        logger.exception(e)
1674
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
1675
        messages.error(request, msg)
1676
    except PermissionDenied, e:
1677
        messages.error(request, e)
1678
    except NotificationError, e:
1679
        rollback = True
1680
        messages.error(request, e)
1681
    else:
1682
        return project_detail(request, id)
1683
    finally:
1684
        if rollback:
1685
            transaction.rollback()
1686
        else:
1687
            transaction.commit()
1688
    return project_search(request)
1689

    
1690

    
1691
@transaction.commit_manually
1692
def handle_project_membership(func):
1693
    @wraps(func)
1694
    def wrapper(request, id, user_id=None):
1695
        rollback = False
1696
        if not user_id:
1697
            user_id = user.id
1698
        try:
1699
            m = ProjectMembership.objects.select_related().get(
1700
                project__application__id=id,
1701
                person__id=user_id)
1702
        except AstakosUser.DoesNotExist:
1703
            return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
1704
        except ProjectMembership.DoesNotExist:
1705
            return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1706
        else:
1707
            try:
1708
                func(request, m)
1709
            except (NotificationError, PermissionDenied), e:
1710
                messages.error(request, e)
1711
                rollback = True
1712
        finally:
1713
            if rollback:
1714
                transaction.rollback()
1715
            else:
1716
                transaction.commit()
1717
        return project_detail(request, id)
1718
    return wrapper
1719

    
1720

    
1721
@require_http_methods(["GET"])
1722
@signed_terms_required
1723
@login_required
1724
@handle_project_membership
1725
def project_leave(request, m):
1726
    m.leave()
1727

    
1728

    
1729
@require_http_methods(["GET"])
1730
@signed_terms_required
1731
@login_required
1732
@handle_project_membership
1733
def project_approve_member(request, m):
1734
    m.accept(request_user=request.user)
1735
    realname = membership.person.realname
1736
    msg = _(astakos_messages.USER_JOINED_GROUP) % locals()
1737
    messages.success(request, msg)
1738

    
1739

    
1740
@require_http_methods(["GET"])
1741
@signed_terms_required
1742
@login_required
1743
@handle_project_membership
1744
def project_remove_member(request, m):
1745
    m.remove(request_user=request.user)
1746
    realname = m.person.realname
1747
    msg = _(astakos_messages.USER_LEFT_GROUP) % locals()
1748
    messages.success(request, msg)
1749

    
1750

    
1751
@require_http_methods(["GET"])
1752
@signed_terms_required
1753
@login_required
1754
@handle_project_membership
1755
def project_reject_member(request, m):
1756
    m.remove(request_user=request.user)
1757
    realname = membership.person.realname
1758
    msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % locals()
1759
    messages.success(request, msg)