Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 40a0cd8b

History | View | Annotate | Download (47.5 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
                    response = prepare_response(request, user, next=next)
389
                    transaction.commit()
390
                    return response
391
                messages.add_message(request, status, message)
392
                transaction.commit()
393
                return render_response(on_success,
394
                                       context_instance=get_context(request, extra_context))
395
            except SendMailError, e:
396
                message = e.message
397
                messages.error(request, message)
398
                transaction.rollback()
399
            except BaseException, e:
400
                message = _('Something went wrong.')
401
                messages.error(request, message)
402
                logger.exception(e)
403
                transaction.rollback()
404
    return render_response(template_name,
405
                           signup_form=form,
406
                           provider=provider,
407
                           context_instance=get_context(request, extra_context))
408

    
409

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

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

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

422
    **Arguments**
423

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

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

431
    **Template:**
432

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

435
    **Settings:**
436

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

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

    
461

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

    
490

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

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

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

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

    
564

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

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

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

    
607

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

    
615

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

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

    
668

    
669

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

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

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

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

    
780

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

    
821

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

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

    
877

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

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

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

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

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

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

    
954

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

    
1010

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

    
1048

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

    
1068

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

    
1090

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

    
1107

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

    
1124

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

    
1139

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

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

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

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

    
1189

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

    
1196

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

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

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

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

    
1225
    print type(start)
1226

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

    
1236

    
1237
def _clear_billing_data(data):
1238

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

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

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

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

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

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