Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 29b87e7c

History | View | Annotate | Download (47.4 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

    
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime, timedelta
43
from collections import defaultdict
44

    
45
from django.contrib import messages
46
from django.contrib.auth.decorators import login_required
47
from django.contrib.auth.views import password_change
48
from django.core.urlresolvers import reverse
49
from django.db import transaction
50
from django.db.models import Q
51
from django.db.utils import IntegrityError
52
from django.forms.fields import URLField
53
from django.http import (HttpResponse, HttpResponseBadRequest,
54
                         HttpResponseForbidden, HttpResponseRedirect,
55
                         HttpResponseBadRequest, Http404)
56
from django.shortcuts import redirect
57
from django.template import RequestContext, loader as template_loader
58
from django.utils.http import urlencode
59
from django.utils.translation import ugettext as _
60
from django.views.generic.create_update import (create_object, delete_object,
61
                                                get_model_and_form_class)
62
from django.views.generic.list_detail import object_list, object_detail
63
from django.http import HttpResponseBadRequest
64
from django.core.xheaders import populate_xheaders
65

    
66
from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
67
                               Resource, EmailChange, GroupKind, Membership,
68
                               AstakosGroupQuota, RESOURCE_SEPARATOR)
69
from django.views.decorators.http import require_http_methods
70

    
71
from astakos.im.activation_backends import get_backend, SimpleBackend
72
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
73
from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
74
                              FeedbackForm, SignApprovalTermsForm,
75
                              ExtendedPasswordChangeForm, EmailChangeForm,
76
                              AstakosGroupCreationForm, AstakosGroupSearchForm,
77
                              AstakosGroupUpdateForm, AddGroupMembersForm,
78
                              AstakosGroupSortForm, MembersSortForm,
79
                              TimelineForm, PickResourceForm,
80
                              AstakosGroupCreationSummaryForm)
81
from astakos.im.functions import (send_feedback, SendMailError,
82
                                  logout as auth_logout,
83
                                  activate as activate_func,
84
                                  switch_account_to_shibboleth,
85
                                  send_group_creation_notification,
86
                                  SendNotificationError)
87
from astakos.im.endpoints.quotaholder import timeline_charge
88
from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
89
                                 LOGGING_LEVEL, PAGINATE_BY)
90
from astakos.im.tasks import request_billing
91
from astakos.im.api.callpoint import AstakosCallpoint
92

    
93
logger = logging.getLogger(__name__)
94

    
95

    
96
DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
97
                                     'https://', '')"""
98

    
99
callpoint = AstakosCallpoint()
100

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

    
118

    
119
def requires_anonymous(func):
120
    """
121
    Decorator checkes whether the request.user is not Anonymous and in that case
122
    redirects to `logout`.
123
    """
124
    @wraps(func)
125
    def wrapper(request, *args):
126
        if not request.user.is_anonymous():
127
            next = urlencode({'next': request.build_absolute_uri()})
128
            logout_uri = reverse(logout) + '?' + next
129
            return HttpResponseRedirect(logout_uri)
130
        return func(request, *args)
131
    return wrapper
132

    
133

    
134
def signed_terms_required(func):
135
    """
136
    Decorator checkes whether the request.user is Anonymous and in that case
137
    redirects to `logout`.
138
    """
139
    @wraps(func)
140
    def wrapper(request, *args, **kwargs):
141
        if request.user.is_authenticated() and not request.user.signed_terms:
142
            params = urlencode({'next': request.build_absolute_uri(),
143
                                'show_form': ''})
144
            terms_uri = reverse('latest_terms') + '?' + params
145
            return HttpResponseRedirect(terms_uri)
146
        return func(request, *args, **kwargs)
147
    return wrapper
148

    
149

    
150
@require_http_methods(["GET", "POST"])
151
@signed_terms_required
152
def index(request, login_template_name='im/login.html', extra_context=None):
153
    """
154
    If there is logged on user renders the profile page otherwise renders login page.
155

156
    **Arguments**
157

158
    ``login_template_name``
159
        A custom login template to use. This is optional; if not specified,
160
        this will default to ``im/login.html``.
161

162
    ``profile_template_name``
163
        A custom profile template to use. This is optional; if not specified,
164
        this will default to ``im/profile.html``.
165

166
    ``extra_context``
167
        An dictionary of variables to add to the template context.
168

169
    **Template:**
170

171
    im/profile.html or im/login.html or ``template_name`` keyword argument.
172

173
    """
174
    template_name = login_template_name
175
    if request.user.is_authenticated():
176
        return HttpResponseRedirect(reverse('edit_profile'))
177
    return render_response(template_name,
178
                           login_form=LoginForm(request=request),
179
                           context_instance=get_context(request, extra_context))
180

    
181

    
182
@require_http_methods(["GET", "POST"])
183
@login_required
184
@signed_terms_required
185
@transaction.commit_manually
186
def invite(request, template_name='im/invitations.html', extra_context=None):
187
    """
188
    Allows a user to invite somebody else.
189

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

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

197
    If the user isn't logged in, redirects to settings.LOGIN_URL.
198

199
    **Arguments**
200

201
    ``template_name``
202
        A custom template to use. This is optional; if not specified,
203
        this will default to ``im/invitations.html``.
204

205
    ``extra_context``
206
        An dictionary of variables to add to the template context.
207

208
    **Template:**
209

210
    im/invitations.html or ``template_name`` keyword argument.
211

212
    **Settings:**
213

214
    The view expectes the following settings are defined:
215

216
    * LOGIN_URL: login uri
217
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
218
    """
219
    status = None
220
    message = None
221
    form = InvitationForm()
222

    
223
    inviter = request.user
224
    if request.method == 'POST':
225
        form = InvitationForm(request.POST)
226
        if inviter.invitations > 0:
227
            if form.is_valid():
228
                try:
229
                    email = form.cleaned_data.get('username')
230
                    realname = form.cleaned_data.get('realname')
231
                    inviter.invite(email, realname)
232
                    message = _('Invitation sent to %s' % email)
233
                    messages.success(request, message)
234
                except SendMailError, e:
235
                    message = e.message
236
                    messages.error(request, message)
237
                    transaction.rollback()
238
                except BaseException, e:
239
                    message = _('Something went wrong.')
240
                    messages.error(request, message)
241
                    logger.exception(e)
242
                    transaction.rollback()
243
                else:
244
                    transaction.commit()
245
        else:
246
            message = _('No invitations left')
247
            messages.error(request, message)
248

    
249
    sent = [{'email': inv.username,
250
             'realname': inv.realname,
251
             'is_consumed': inv.is_consumed}
252
            for inv in request.user.invitations_sent.all()]
253
    kwargs = {'inviter': inviter,
254
              'sent': sent}
255
    context = get_context(request, extra_context, **kwargs)
256
    return render_response(template_name,
257
                           invitation_form=form,
258
                           context_instance=context)
259

    
260

    
261
@require_http_methods(["GET", "POST"])
262
@login_required
263
@signed_terms_required
264
def edit_profile(request, template_name='im/profile.html', extra_context=None):
265
    """
266
    Allows a user to edit his/her profile.
267

268
    In case of GET request renders a form for displaying the user information.
269
    In case of POST updates the user informantion and redirects to ``next``
270
    url parameter if exists.
271

272
    If the user isn't logged in, redirects to settings.LOGIN_URL.
273

274
    **Arguments**
275

276
    ``template_name``
277
        A custom template to use. This is optional; if not specified,
278
        this will default to ``im/profile.html``.
279

280
    ``extra_context``
281
        An dictionary of variables to add to the template context.
282

283
    **Template:**
284

285
    im/profile.html or ``template_name`` keyword argument.
286

287
    **Settings:**
288

289
    The view expectes the following settings are defined:
290

291
    * LOGIN_URL: login uri
292
    """
293
    extra_context = extra_context or {}
294
    form = ProfileForm(instance=request.user)
295
    extra_context['next'] = request.GET.get('next')
296
    reset_cookie = False
297
    if request.method == 'POST':
298
        form = ProfileForm(request.POST, instance=request.user)
299
        if form.is_valid():
300
            try:
301
                prev_token = request.user.auth_token
302
                user = form.save()
303
                reset_cookie = user.auth_token != prev_token
304
                form = ProfileForm(instance=user)
305
                next = request.POST.get('next')
306
                if next:
307
                    return redirect(next)
308
                msg = _('Profile has been updated successfully')
309
                messages.success(request, msg)
310
            except ValueError, ve:
311
                messages.success(request, ve)
312
    elif request.method == "GET":
313
        if not request.user.is_verified:
314
            request.user.is_verified = True
315
            request.user.save()
316
    return render_response(template_name,
317
                           reset_cookie=reset_cookie,
318
                           profile_form=form,
319
                           context_instance=get_context(request,
320
                                                        extra_context))
321

    
322

    
323
@transaction.commit_manually
324
@require_http_methods(["GET", "POST"])
325
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
326
    """
327
    Allows a user to create a local account.
328

329
    In case of GET request renders a form for entering the user information.
330
    In case of POST handles the signup.
331

332
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
333
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
334
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
335
    (see activation_backends);
336

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

340
    On unsuccessful creation, renders ``template_name`` with an error message.
341

342
    **Arguments**
343

344
    ``template_name``
345
        A custom template to render. This is optional;
346
        if not specified, this will default to ``im/signup.html``.
347

348
    ``on_success``
349
        A custom template to render in case of success. This is optional;
350
        if not specified, this will default to ``im/signup_complete.html``.
351

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

355
    **Template:**
356

357
    im/signup.html or ``template_name`` keyword argument.
358
    im/signup_complete.html or ``on_success`` keyword argument.
359
    """
360
    if request.user.is_authenticated():
361
        return HttpResponseRedirect(reverse('edit_profile'))
362

    
363
    provider = get_query(request).get('provider', 'local')
364
    try:
365
        if not backend:
366
            backend = get_backend(request)
367
        form = backend.get_signup_form(provider)
368
    except Exception, e:
369
        form = SimpleBackend(request).get_signup_form(provider)
370
        messages.error(request, e)
371
    if request.method == 'POST':
372
        if form.is_valid():
373
            user = form.save(commit=False)
374
            try:
375
                result = backend.handle_activation(user)
376
                status = messages.SUCCESS
377
                message = result.message
378
                user.save()
379
                if 'additional_email' in form.cleaned_data:
380
                    additional_email = form.cleaned_data['additional_email']
381
                    if additional_email != user.email:
382
                        user.additionalmail_set.create(email=additional_email)
383
                        msg = 'Additional email: %s saved for user %s.' % (
384
                            additional_email, user.email)
385
                        logger.log(LOGGING_LEVEL, msg)
386
                if user and user.is_active:
387
                    next = request.POST.get('next', '')
388
                    transaction.commit()
389
                    return prepare_response(request, user, next=next)
390
                messages.add_message(request, status, message)
391
                transaction.commit()
392
                return render_response(on_success,
393
                                       context_instance=get_context(request, extra_context))
394
            except SendMailError, e:
395
                message = e.message
396
                messages.error(request, message)
397
                transaction.rollback()
398
            except BaseException, e:
399
                message = _('Something went wrong.')
400
                messages.error(request, message)
401
                logger.exception(e)
402
                transaction.rollback()
403
    return render_response(template_name,
404
                           signup_form=form,
405
                           provider=provider,
406
                           context_instance=get_context(request, extra_context))
407

    
408

    
409
@require_http_methods(["GET", "POST"])
410
@login_required
411
@signed_terms_required
412
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
413
    """
414
    Allows a user to send feedback.
415

416
    In case of GET request renders a form for providing the feedback information.
417
    In case of POST sends an email to support team.
418

419
    If the user isn't logged in, redirects to settings.LOGIN_URL.
420

421
    **Arguments**
422

423
    ``template_name``
424
        A custom template to use. This is optional; if not specified,
425
        this will default to ``im/feedback.html``.
426

427
    ``extra_context``
428
        An dictionary of variables to add to the template context.
429

430
    **Template:**
431

432
    im/signup.html or ``template_name`` keyword argument.
433

434
    **Settings:**
435

436
    * LOGIN_URL: login uri
437
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
438
    """
439
    if request.method == 'GET':
440
        form = FeedbackForm()
441
    if request.method == 'POST':
442
        if not request.user:
443
            return HttpResponse('Unauthorized', status=401)
444

    
445
        form = FeedbackForm(request.POST)
446
        if form.is_valid():
447
            msg = form.cleaned_data['feedback_msg']
448
            data = form.cleaned_data['feedback_data']
449
            try:
450
                send_feedback(msg, data, request.user, email_template_name)
451
            except SendMailError, e:
452
                messages.error(request, message)
453
            else:
454
                message = _('Feedback successfully sent')
455
                messages.success(request, message)
456
    return render_response(template_name,
457
                           feedback_form=form,
458
                           context_instance=get_context(request, extra_context))
459

    
460

    
461
@require_http_methods(["GET", "POST"])
462
@signed_terms_required
463
def logout(request, template='registration/logged_out.html', extra_context=None):
464
    """
465
    Wraps `django.contrib.auth.logout` and delete the cookie.
466
    """
467
    response = HttpResponse()
468
    if request.user.is_authenticated():
469
        email = request.user.email
470
        auth_logout(request)
471
        response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
472
        msg = 'Cookie deleted for %s' % email
473
        logger.log(LOGGING_LEVEL, msg)
474
    next = request.GET.get('next')
475
    if next:
476
        response['Location'] = next
477
        response.status_code = 302
478
        return response
479
    elif LOGOUT_NEXT:
480
        response['Location'] = LOGOUT_NEXT
481
        response.status_code = 301
482
        return response
483
    messages.success(request, _('You have successfully logged out.'))
484
    context = get_context(request, extra_context)
485
    response.write(
486
        template_loader.render_to_string(template, context_instance=context))
487
    return response
488

    
489

    
490
@require_http_methods(["GET", "POST"])
491
@transaction.commit_manually
492
def activate(request, greeting_email_template_name='im/welcome_email.txt',
493
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
494
    """
495
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
496
    and renews the user token.
497

498
    The view uses commit_manually decorator in order to ensure the user state will be updated
499
    only if the email will be send successfully.
500
    """
501
    token = request.GET.get('auth')
502
    next = request.GET.get('next')
503
    try:
504
        user = AstakosUser.objects.get(auth_token=token)
505
    except AstakosUser.DoesNotExist:
506
        return HttpResponseBadRequest(_('No such user'))
507

    
508
    if user.is_active:
509
        message = _('Account already active.')
510
        messages.error(request, message)
511
        return index(request)
512

    
513
    try:
514
        local_user = AstakosUser.objects.get(
515
            ~Q(id=user.id),
516
            email=user.email,
517
            is_active=True
518
        )
519
    except AstakosUser.DoesNotExist:
520
        try:
521
            activate_func(
522
                user,
523
                greeting_email_template_name,
524
                helpdesk_email_template_name,
525
                verify_email=True
526
            )
527
            response = prepare_response(request, user, next, renew=True)
528
            transaction.commit()
529
            return response
530
        except SendMailError, e:
531
            message = e.message
532
            messages.error(request, message)
533
            transaction.rollback()
534
            return index(request)
535
        except BaseException, e:
536
            message = _('Something went wrong.')
537
            messages.error(request, message)
538
            logger.exception(e)
539
            transaction.rollback()
540
            return index(request)
541
    else:
542
        try:
543
            user = switch_account_to_shibboleth(
544
                user,
545
                local_user,
546
                greeting_email_template_name
547
            )
548
            response = prepare_response(request, user, next, renew=True)
549
            transaction.commit()
550
            return response
551
        except SendMailError, e:
552
            message = e.message
553
            messages.error(request, message)
554
            transaction.rollback()
555
            return index(request)
556
        except BaseException, e:
557
            message = _('Something went wrong.')
558
            messages.error(request, message)
559
            logger.exception(e)
560
            transaction.rollback()
561
            return index(request)
562

    
563

    
564
@require_http_methods(["GET", "POST"])
565
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
566
    term = None
567
    terms = None
568
    if not term_id:
569
        try:
570
            term = ApprovalTerms.objects.order_by('-id')[0]
571
        except IndexError:
572
            pass
573
    else:
574
        try:
575
            term = ApprovalTerms.objects.get(id=term_id)
576
        except ApprovalTerms.DoesNotExist, e:
577
            pass
578

    
579
    if not term:
580
        messages.error(request, 'There are no approval terms.')
581
        return HttpResponseRedirect(reverse('index'))
582
    f = open(term.location, 'r')
583
    terms = f.read()
584

    
585
    if request.method == 'POST':
586
        next = request.POST.get('next')
587
        if not next:
588
            next = reverse('index')
589
        form = SignApprovalTermsForm(request.POST, instance=request.user)
590
        if not form.is_valid():
591
            return render_response(template_name,
592
                                   terms=terms,
593
                                   approval_terms_form=form,
594
                                   context_instance=get_context(request, extra_context))
595
        user = form.save()
596
        return HttpResponseRedirect(next)
597
    else:
598
        form = None
599
        if request.user.is_authenticated() and not request.user.signed_terms:
600
            form = SignApprovalTermsForm(instance=request.user)
601
        return render_response(template_name,
602
                               terms=terms,
603
                               approval_terms_form=form,
604
                               context_instance=get_context(request, extra_context))
605

    
606

    
607
@require_http_methods(["GET", "POST"])
608
@signed_terms_required
609
def change_password(request):
610
    return password_change(request,
611
                           post_change_redirect=reverse('edit_profile'),
612
                           password_change_form=ExtendedPasswordChangeForm)
613

    
614

    
615
@require_http_methods(["GET", "POST"])
616
@signed_terms_required
617
@login_required
618
@transaction.commit_manually
619
def change_email(request, activation_key=None,
620
                 email_template_name='registration/email_change_email.txt',
621
                 form_template_name='registration/email_change_form.html',
622
                 confirm_template_name='registration/email_change_done.html',
623
                 extra_context=None):
624
    if activation_key:
625
        try:
626
            user = EmailChange.objects.change_email(activation_key)
627
            if request.user.is_authenticated() and request.user == user:
628
                msg = _('Email changed successfully.')
629
                messages.success(request, msg)
630
                auth_logout(request)
631
                response = prepare_response(request, user)
632
                transaction.commit()
633
                return response
634
        except ValueError, e:
635
            messages.error(request, e)
636
        return render_response(confirm_template_name,
637
                               modified_user=user if 'user' in locals(
638
                               ) else None,
639
                               context_instance=get_context(request,
640
                                                            extra_context))
641

    
642
    if not request.user.is_authenticated():
643
        path = quote(request.get_full_path())
644
        url = request.build_absolute_uri(reverse('index'))
645
        return HttpResponseRedirect(url + '?next=' + path)
646
    form = EmailChangeForm(request.POST or None)
647
    if request.method == 'POST' and form.is_valid():
648
        try:
649
            ec = form.save(email_template_name, request)
650
        except SendMailError, e:
651
            msg = e
652
            messages.error(request, msg)
653
            transaction.rollback()
654
        except IntegrityError, e:
655
            msg = _('There is already a pending change email request.')
656
            messages.error(request, msg)
657
        else:
658
            msg = _('Change email request has been registered succefully.\
659
                    You are going to receive a verification email in the new address.')
660
            messages.success(request, msg)
661
            transaction.commit()
662
    return render_response(form_template_name,
663
                           form=form,
664
                           context_instance=get_context(request,
665
                                                        extra_context))
666

    
667

    
668

    
669
resource_presentation = {
670
       'compute': {
671
            'help_text':'group compute help text',
672
            'is_abbreviation':False,
673
            'report_desc':''
674
        },
675
        'storage': {
676
            'help_text':'group storage help text',
677
            'is_abbreviation':False,
678
            'report_desc':''
679
        },
680
        'pithos+.diskspace': {
681
            'help_text':'resource pithos+.diskspace help text',
682
            'is_abbreviation':False,
683
            'report_desc':'Diskspace used'
684
        },
685
        'cyclades.vm': {
686
            'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
687
            'is_abbreviation':True,
688
            'report_desc':'Number of Virtual Machines'
689
        },
690
        'cyclades.disksize': {
691
            'help_text':'resource cyclades.disksize help text',
692
            'is_abbreviation':False,
693
            'report_desc':'Amount of Disksize used'
694
        },
695
        'cyclades.ram': {
696
            'help_text':'resource cyclades.ram help text',
697
            'is_abbreviation':True,
698
            'report_desc':'RAM used'
699
        },
700
        'cyclades.cpu': {
701
            'help_text':'resource cyclades.cpu help text',
702
            'is_abbreviation':True,
703
            'report_desc':'CPUs used'
704
        }
705
    }
706

    
707
@require_http_methods(["GET", "POST"])
708
@signed_terms_required
709
@login_required
710
def group_add(request, kind_name='default'):
711
    result = callpoint.list_resources()
712
    resource_catalog = {'resources':defaultdict(defaultdict),
713
                        'groups':defaultdict(list)}
714
    if result.is_success:
715
        for r in result.data:
716
            service = r.get('service', '')
717
            name = r.get('name', '')
718
            group = r.get('group', '')
719
            unit = r.get('unit', '')
720
            fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
721
            resource_catalog['resources'][fullname] = dict(unit=unit)
722
            resource_catalog['groups'][group].append(fullname)
723
        
724
        resource_catalog = dict(resource_catalog)
725
        for k, v in resource_catalog.iteritems():
726
            resource_catalog[k] = dict(v)
727
    else:
728
        messages.error(
729
            request,
730
            'Unable to retrieve system resources: %s' % result.reason
731
    )
732
    
733
    try:
734
        kind = GroupKind.objects.get(name=kind_name)
735
    except:
736
        return HttpResponseBadRequest(_('No such group kind'))
737
    
738
    
739

    
740
    post_save_redirect = '/im/group/%(id)s/'
741
    context_processors = None
742
    model, form_class = get_model_and_form_class(
743
        model=None,
744
        form_class=AstakosGroupCreationForm
745
    )
746
    
747
    if request.method == 'POST':
748
        form = form_class(request.POST, request.FILES)
749
        if form.is_valid():
750
            return render_response(
751
                template='im/astakosgroup_form_summary.html',
752
                context_instance=get_context(request),
753
                form = AstakosGroupCreationSummaryForm(form.cleaned_data),
754
                policies = form.policies(),
755
                resource_catalog=resource_catalog,
756
                resource_presentation=resource_presentation
757
            )
758
    else:
759
        now = datetime.now()
760
        data = {
761
            'kind': kind
762
        }
763
        form = form_class(data)
764

    
765
    # Create the template, context, response
766
    template_name = "%s/%s_form_demo.html" % (
767
        model._meta.app_label,
768
        model._meta.object_name.lower()
769
    )
770
    t = template_loader.get_template(template_name)
771
    c = RequestContext(request, {
772
        'form': form,
773
        'kind': kind,
774
        'resource_catalog':resource_catalog,
775
        'resource_presentation':resource_presentation,
776
    }, context_processors)
777
    return HttpResponse(t.render(c))
778

    
779

    
780
@require_http_methods(["POST"])
781
@signed_terms_required
782
@login_required
783
def group_add_complete(request):
784
    model = AstakosGroup
785
    form = AstakosGroupCreationSummaryForm(request.POST)
786
    if form.is_valid():
787
        d = form.cleaned_data
788
        d['owners'] = [request.user]
789
        result = callpoint.create_groups((d,)).next()
790
        if result.is_success:
791
            new_object = result.data[0]
792
            msg = _("The %(verbose_name)s was created successfully.") %\
793
                {"verbose_name": model._meta.verbose_name}
794
            messages.success(request, msg, fail_silently=True)
795
            
796
            # send notification
797
            try:
798
                send_group_creation_notification(
799
                    template_name='im/group_creation_notification.txt',
800
                    dictionary={
801
                        'group': new_object,
802
                        'owner': request.user,
803
                        'policies': d.get('policies', [])
804
                    }
805
                )
806
            except SendNotificationError, e:
807
                messages.error(request, e, fail_silently=True)
808
            post_save_redirect = '/im/group/%(id)s/'
809
            return HttpResponseRedirect(post_save_redirect % new_object)
810
        else:
811
            msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
812
                {"verbose_name": model._meta.verbose_name,
813
                 "reason":result.reason}
814
            messages.error(request, msg, fail_silently=True)
815
    return render_response(
816
        template='im/astakosgroup_form_summary.html',
817
        context_instance=get_context(request),
818
        form=form)
819

    
820

    
821
@require_http_methods(["GET"])
822
@signed_terms_required
823
@login_required
824
def group_list(request):
825
    none = request.user.astakos_groups.none()
826
    q = AstakosGroup.objects.raw("""
827
        SELECT auth_group.id,
828
        %s AS groupname,
829
        im_groupkind.name AS kindname,
830
        im_astakosgroup.*,
831
        owner.email AS groupowner,
832
        (SELECT COUNT(*) FROM im_membership
833
            WHERE group_id = im_astakosgroup.group_ptr_id
834
            AND date_joined IS NOT NULL) AS approved_members_num,
835
        (SELECT CASE WHEN(
836
                    SELECT date_joined FROM im_membership
837
                    WHERE group_id = im_astakosgroup.group_ptr_id
838
                    AND person_id = %s) IS NULL
839
                    THEN 0 ELSE 1 END) AS membership_status
840
        FROM im_astakosgroup
841
        INNER JOIN im_membership ON (
842
            im_astakosgroup.group_ptr_id = im_membership.group_id)
843
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
844
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
845
        LEFT JOIN im_astakosuser_owner ON (
846
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
847
        LEFT JOIN auth_user as owner ON (
848
            im_astakosuser_owner.astakosuser_id = owner.id)
849
        WHERE im_membership.person_id = %s
850
        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
851
    d = defaultdict(list)
852
    for g in q:
853
        if request.user.email == g.groupowner:
854
            d['own'].append(g)
855
        else:
856
            d['other'].append(g)
857

    
858
    # validate sorting
859
    fields = ('own', 'other')
860
    for f in fields:
861
        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
862
        if v:
863
            form = AstakosGroupSortForm({'sort_by': v})
864
            if not form.is_valid():
865
                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
866
    return object_list(request, queryset=none,
867
                       extra_context={'is_search': False,
868
                                      'mine': d['own'],
869
                                      'other': d['other'],
870
                                      'own_sorting': own_sorting,
871
                                      'other_sorting': other_sorting,
872
                                      'own_page': request.GET.get('own_page', 1),
873
                                      'other_page': request.GET.get('other_page', 1)
874
                                      })
875

    
876

    
877
@require_http_methods(["GET", "POST"])
878
@signed_terms_required
879
@login_required
880
def group_detail(request, group_id):
881
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
882
    q = q.extra(select={
883
        'is_member': """SELECT CASE WHEN EXISTS(
884
                            SELECT id FROM im_membership
885
                            WHERE group_id = im_astakosgroup.group_ptr_id
886
                            AND person_id = %s)
887
                        THEN 1 ELSE 0 END""" % request.user.id,
888
        'is_owner': """SELECT CASE WHEN EXISTS(
889
                        SELECT id FROM im_astakosuser_owner
890
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
891
                        AND astakosuser_id = %s)
892
                        THEN 1 ELSE 0 END""" % request.user.id,
893
        'kindname': """SELECT name FROM im_groupkind
894
                       WHERE id = im_astakosgroup.kind_id"""})
895

    
896
    model = q.model
897
    context_processors = None
898
    mimetype = None
899
    try:
900
        obj = q.get()
901
    except AstakosGroup.DoesNotExist:
902
        raise Http404("No %s found matching the query" % (
903
            model._meta.verbose_name))
904

    
905
    update_form = AstakosGroupUpdateForm(instance=obj)
906
    addmembers_form = AddGroupMembersForm()
907
    if request.method == 'POST':
908
        update_data = {}
909
        addmembers_data = {}
910
        for k, v in request.POST.iteritems():
911
            if k in update_form.fields:
912
                update_data[k] = v
913
            if k in addmembers_form.fields:
914
                addmembers_data[k] = v
915
        update_data = update_data or None
916
        addmembers_data = addmembers_data or None
917
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
918
        addmembers_form = AddGroupMembersForm(addmembers_data)
919
        if update_form.is_valid():
920
            update_form.save()
921
        if addmembers_form.is_valid():
922
            map(obj.approve_member, addmembers_form.valid_users)
923
            addmembers_form = AddGroupMembersForm()
924

    
925
    template_name = "%s/%s_detail.html" % (
926
        model._meta.app_label, model._meta.object_name.lower())
927
    t = template_loader.get_template(template_name)
928
    c = RequestContext(request, {
929
        'object': obj,
930
    }, context_processors)
931

    
932
    # validate sorting
933
    sorting = request.GET.get('sorting')
934
    if sorting:
935
        form = MembersSortForm({'sort_by': sorting})
936
        if form.is_valid():
937
            sorting = form.cleaned_data.get('sort_by')
938

    
939
    extra_context = {'update_form': update_form,
940
                     'addmembers_form': addmembers_form,
941
                     'page': request.GET.get('page', 1),
942
                     'sorting': sorting}
943
    for key, value in extra_context.items():
944
        if callable(value):
945
            c[key] = value()
946
        else:
947
            c[key] = value
948
    response = HttpResponse(t.render(c), mimetype=mimetype)
949
    populate_xheaders(
950
        request, response, model, getattr(obj, obj._meta.pk.name))
951
    return response
952

    
953

    
954
@require_http_methods(["GET", "POST"])
955
@signed_terms_required
956
@login_required
957
def group_search(request, extra_context=None, **kwargs):
958
    q = request.GET.get('q')
959
    sorting = request.GET.get('sorting')
960
    if request.method == 'GET':
961
        form = AstakosGroupSearchForm({'q': q} if q else None)
962
    else:
963
        form = AstakosGroupSearchForm(get_query(request))
964
        if form.is_valid():
965
            q = form.cleaned_data['q'].strip()
966
    if q:
967
        queryset = AstakosGroup.objects.select_related()
968
        queryset = queryset.filter(name__contains=q)
969
        queryset = queryset.filter(approval_date__isnull=False)
970
        queryset = queryset.extra(select={
971
                                  'groupname': DB_REPLACE_GROUP_SCHEME,
972
                                  'kindname': "im_groupkind.name",
973
                                  'approved_members_num': """
974
                    SELECT COUNT(*) FROM im_membership
975
                    WHERE group_id = im_astakosgroup.group_ptr_id
976
                    AND date_joined IS NOT NULL""",
977
                                  'membership_approval_date': """
978
                    SELECT date_joined FROM im_membership
979
                    WHERE group_id = im_astakosgroup.group_ptr_id
980
                    AND person_id = %s""" % request.user.id,
981
                                  'is_member': """
982
                    SELECT CASE WHEN EXISTS(
983
                    SELECT date_joined FROM im_membership
984
                    WHERE group_id = im_astakosgroup.group_ptr_id
985
                    AND person_id = %s)
986
                    THEN 1 ELSE 0 END""" % request.user.id,
987
                                  'is_owner': """
988
                    SELECT CASE WHEN EXISTS(
989
                    SELECT id FROM im_astakosuser_owner
990
                    WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
991
                    AND astakosuser_id = %s)
992
                    THEN 1 ELSE 0 END""" % request.user.id})
993
        if sorting:
994
            # TODO check sorting value
995
            queryset = queryset.order_by(sorting)
996
    else:
997
        queryset = AstakosGroup.objects.none()
998
    return object_list(
999
        request,
1000
        queryset,
1001
        paginate_by=PAGINATE_BY,
1002
        page=request.GET.get('page') or 1,
1003
        template_name='im/astakosgroup_list.html',
1004
        extra_context=dict(form=form,
1005
                           is_search=True,
1006
                           q=q,
1007
                           sorting=sorting))
1008

    
1009

    
1010
@require_http_methods(["POST"])
1011
@signed_terms_required
1012
@login_required
1013
def group_all(request, extra_context=None, **kwargs):
1014
    q = AstakosGroup.objects.select_related()
1015
    q = q.filter(approval_date__isnull=False)
1016
    q = q.extra(select={
1017
                'groupname': DB_REPLACE_GROUP_SCHEME,
1018
                'kindname': "im_groupkind.name",
1019
                'approved_members_num': """
1020
                    SELECT COUNT(*) FROM im_membership
1021
                    WHERE group_id = im_astakosgroup.group_ptr_id
1022
                    AND date_joined IS NOT NULL""",
1023
                'membership_approval_date': """
1024
                    SELECT date_joined FROM im_membership
1025
                    WHERE group_id = im_astakosgroup.group_ptr_id
1026
                    AND person_id = %s""" % request.user.id,
1027
                'is_member': """
1028
                    SELECT CASE WHEN EXISTS(
1029
                    SELECT date_joined FROM im_membership
1030
                    WHERE group_id = im_astakosgroup.group_ptr_id
1031
                    AND person_id = %s)
1032
                    THEN 1 ELSE 0 END""" % request.user.id})
1033
    sorting = request.GET.get('sorting')
1034
    if sorting:
1035
        # TODO check sorting value
1036
        q = q.order_by(sorting)
1037
    return object_list(
1038
        request,
1039
        q,
1040
        paginate_by=PAGINATE_BY,
1041
        page=request.GET.get('page') or 1,
1042
        template_name='im/astakosgroup_list.html',
1043
        extra_context=dict(form=AstakosGroupSearchForm(),
1044
                           is_search=True,
1045
                           sorting=sorting))
1046

    
1047

    
1048
@require_http_methods(["POST"])
1049
@signed_terms_required
1050
@login_required
1051
def group_join(request, group_id):
1052
    m = Membership(group_id=group_id,
1053
                   person=request.user,
1054
                   date_requested=datetime.now())
1055
    try:
1056
        m.save()
1057
        post_save_redirect = reverse(
1058
            'group_detail',
1059
            kwargs=dict(group_id=group_id))
1060
        return HttpResponseRedirect(post_save_redirect)
1061
    except IntegrityError, e:
1062
        logger.exception(e)
1063
        msg = _('Failed to join group.')
1064
        messages.error(request, msg)
1065
        return group_search(request)
1066

    
1067

    
1068
@require_http_methods(["POST"])
1069
@signed_terms_required
1070
@login_required
1071
def group_leave(request, group_id):
1072
    try:
1073
        m = Membership.objects.select_related().get(
1074
            group__id=group_id,
1075
            person=request.user)
1076
    except Membership.DoesNotExist:
1077
        return HttpResponseBadRequest(_('Invalid membership.'))
1078
    if request.user in m.group.owner.all():
1079
        return HttpResponseForbidden(_('Owner can not leave the group.'))
1080
    return delete_object(
1081
        request,
1082
        model=Membership,
1083
        object_id=m.id,
1084
        template_name='im/astakosgroup_list.html',
1085
        post_delete_redirect=reverse(
1086
            'group_detail',
1087
            kwargs=dict(group_id=group_id)))
1088

    
1089

    
1090
def handle_membership(func):
1091
    @wraps(func)
1092
    def wrapper(request, group_id, user_id):
1093
        try:
1094
            m = Membership.objects.select_related().get(
1095
                group__id=group_id,
1096
                person__id=user_id)
1097
        except Membership.DoesNotExist:
1098
            return HttpResponseBadRequest(_('Invalid membership.'))
1099
        else:
1100
            if request.user not in m.group.owner.all():
1101
                return HttpResponseForbidden(_('User is not a group owner.'))
1102
            func(request, m)
1103
            return group_detail(request, group_id)
1104
    return wrapper
1105

    
1106

    
1107
@require_http_methods(["POST"])
1108
@signed_terms_required
1109
@login_required
1110
@handle_membership
1111
def approve_member(request, membership):
1112
    try:
1113
        membership.approve()
1114
        realname = membership.person.realname
1115
        msg = _('%s has been successfully joined the group.' % realname)
1116
        messages.success(request, msg)
1117
    except BaseException, e:
1118
        logger.exception(e)
1119
        realname = membership.person.realname
1120
        msg = _('Something went wrong during %s\'s approval.' % realname)
1121
        messages.error(request, msg)
1122

    
1123

    
1124
@signed_terms_required
1125
@login_required
1126
@handle_membership
1127
def disapprove_member(request, membership):
1128
    try:
1129
        membership.disapprove()
1130
        realname = membership.person.realname
1131
        msg = _('%s has been successfully removed from the group.' % realname)
1132
        messages.success(request, msg)
1133
    except BaseException, e:
1134
        logger.exception(e)
1135
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
1136
        messages.error(request, msg)
1137

    
1138

    
1139
@require_http_methods(["GET"])
1140
@signed_terms_required
1141
@login_required
1142
def resource_list(request):
1143
#     if request.method == 'POST':
1144
#         form = PickResourceForm(request.POST)
1145
#         if form.is_valid():
1146
#             r = form.cleaned_data.get('resource')
1147
#             if r:
1148
#                 groups = request.user.membership_set.only('group').filter(
1149
#                     date_joined__isnull=False)
1150
#                 groups = [g.group_id for g in groups]
1151
#                 q = AstakosGroupQuota.objects.select_related().filter(
1152
#                     resource=r, group__in=groups)
1153
#     else:
1154
#         form = PickResourceForm()
1155
#         q = AstakosGroupQuota.objects.none()
1156
#
1157
#     return object_list(request, q,
1158
#                        template_name='im/astakosuserquota_list.html',
1159
#                        extra_context={'form': form, 'data':data})
1160

    
1161
    def with_class(entry):
1162
        entry['load_class'] = 'red'
1163
        max_value = float(entry['maxValue'])
1164
        curr_value = float(entry['currValue'])
1165
        entry['ratio'] = (curr_value / max_value) * 100
1166
        if entry['ratio'] < 66:
1167
            entry['load_class'] = 'yellow'
1168
        if entry['ratio'] < 33:
1169
            entry['load_class'] = 'green'
1170
        return entry
1171

    
1172
    def pluralize(entry):
1173
        entry['plural'] = engine.plural(entry.get('name'))
1174
        return entry
1175

    
1176
    result = callpoint.get_user_status(request.user.id)
1177
    if result.is_success:
1178
        backenddata = map(with_class, result.data)
1179
        data = map(pluralize, result.data)
1180
    else:
1181
        data = None
1182
        messages.error(request, result.reason)
1183
    return render_response('im/resource_list.html',
1184
                           data=data,
1185
                           resource_presentation=resource_presentation,
1186
                           context_instance=get_context(request))
1187

    
1188

    
1189
def group_create_list(request):
1190
    form = PickResourceForm()
1191
    return render_response(
1192
        template='im/astakosgroup_create_list.html',
1193
        context_instance=get_context(request),)
1194

    
1195

    
1196
@require_http_methods(["GET"])
1197
@signed_terms_required
1198
@login_required
1199
def billing(request):
1200

    
1201
    today = datetime.today()
1202
    month_last_day = calendar.monthrange(today.year, today.month)[1]
1203
    data['resources'] = map(with_class, data['resources'])
1204
    start = request.POST.get('datefrom', None)
1205
    if start:
1206
        today = datetime.fromtimestamp(int(start))
1207
        month_last_day = calendar.monthrange(today.year, today.month)[1]
1208

    
1209
    start = datetime(today.year, today.month, 1).strftime("%s")
1210
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1211
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1212
                                    int(start) * 1000,
1213
                                    int(end) * 1000))
1214
    data = {}
1215

    
1216
    try:
1217
        status, data = r.result
1218
        data = _clear_billing_data(data)
1219
        if status != 200:
1220
            messages.error(request, _('Service response status: %d' % status))
1221
    except:
1222
        messages.error(request, r.result)
1223

    
1224
    print type(start)
1225

    
1226
    return render_response(
1227
        template='im/billing.html',
1228
        context_instance=get_context(request),
1229
        data=data,
1230
        zerodate=datetime(month=1, year=1970, day=1),
1231
        today=today,
1232
        start=int(start),
1233
        month_last_day=month_last_day)
1234

    
1235

    
1236
def _clear_billing_data(data):
1237

    
1238
    # remove addcredits entries
1239
    def isnotcredit(e):
1240
        return e['serviceName'] != "addcredits"
1241

    
1242
    # separate services
1243
    def servicefilter(service_name):
1244
        service = service_name
1245

    
1246
        def fltr(e):
1247
            return e['serviceName'] == service
1248
        return fltr
1249

    
1250
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1251
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1252
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1253
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1254

    
1255
    return data
1256
     
1257
     
1258
@require_http_methods(["GET"])
1259
@signed_terms_required
1260
@login_required
1261
def timeline(request):
1262
#    data = {'entity':request.user.email}
1263
    timeline_body = ()
1264
    timeline_header = ()
1265
#    form = TimelineForm(data)
1266
    form = TimelineForm()
1267
    if request.method == 'POST':
1268
        data = request.POST
1269
        form = TimelineForm(data)
1270
        if form.is_valid():
1271
            data = form.cleaned_data
1272
            timeline_header = ('entity', 'resource',
1273
                               'event name', 'event date',
1274
                               'incremental cost', 'total cost')
1275
            timeline_body = timeline_charge(
1276
                data['entity'], data['resource'],
1277
                data['start_date'], data['end_date'],
1278
                data['details'], data['operation'])
1279

    
1280
    return render_response(template='im/timeline.html',
1281
                           context_instance=get_context(request),
1282
                           form=form,
1283
                           timeline_header=timeline_header,
1284
                           timeline_body=timeline_body)
1285
    return data