Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 12269b45

History | View | Annotate | Download (47.2 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
from astakos.im.functions import (send_feedback, SendMailError,
81
                                  logout as auth_logout,
82
                                  activate as activate_func,
83
                                  switch_account_to_shibboleth,
84
                                  send_group_creation_notification,
85
                                  SendNotificationError)
86
from astakos.im.endpoints.quotaholder import timeline_charge
87
from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
88
                                 LOGGING_LEVEL, PAGINATE_BY)
89
from astakos.im.tasks import request_billing
90
from astakos.im.api.callpoint import AstakosCallpoint
91

    
92
logger = logging.getLogger(__name__)
93

    
94

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

    
98
callpoint = AstakosCallpoint()
99

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

    
117

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

    
132

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

    
148

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

155
    **Arguments**
156

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

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

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

168
    **Template:**
169

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

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

    
180

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

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

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

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

198
    **Arguments**
199

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

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

207
    **Template:**
208

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

211
    **Settings:**
212

213
    The view expectes the following settings are defined:
214

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

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

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

    
259

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

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

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

273
    **Arguments**
274

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

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

282
    **Template:**
283

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

286
    **Settings:**
287

288
    The view expectes the following settings are defined:
289

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

    
321

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

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

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

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

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

341
    **Arguments**
342

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

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

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

354
    **Template:**
355

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

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

    
407

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

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

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

420
    **Arguments**
421

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

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

429
    **Template:**
430

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

433
    **Settings:**
434

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

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

    
459

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

    
488

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

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

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

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

    
562

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

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

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

    
605

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

    
613

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

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

    
666

    
667

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

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

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

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

    
777

    
778
# @require_http_methods(["POST"])
779
# @signed_terms_required
780
# @login_required
781
def group_add_complete(request):
782
    d = dict(request.POST)
783
    d['owners'] = [request.user]
784
    result = callpoint.create_groups((d,)).next()
785
    if result.is_success:
786
        new_object = result.data[0]
787
        model = AstakosGroup
788
        msg = _("The %(verbose_name)s was created successfully.") %\
789
            {"verbose_name": model._meta.verbose_name}
790
        messages.success(request, msg, fail_silently=True)
791

    
792
#                # send notification
793
#                 try:
794
#                     send_group_creation_notification(
795
#                         template_name='im/group_creation_notification.txt',
796
#                         dictionary={
797
#                             'group': new_object,
798
#                             'owner': request.user,
799
#                             'policies': list(form.cleaned_data['policies']),
800
#                         }
801
#                     )
802
#                 except SendNotificationError, e:
803
#                     messages.error(request, e, fail_silently=True)
804
        post_save_redirect = '/im/group/%(id)s/'
805
        return HttpResponseRedirect(post_save_redirect % new_object)
806
    else:
807
        msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
808
            {"verbose_name": model._meta.verbose_name,
809
             "reason":result.reason}
810
        messages.error(request, msg, fail_silently=True)
811
    
812
    return render_response(
813
    template='im/astakosgroup_form_summary.html',
814
    context_instance=get_context(request))
815

    
816

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

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

    
872

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

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

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

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

    
928
    # validate sorting
929
    sorting = request.GET.get('sorting')
930
    if sorting:
931
        form = MembersSortForm({'sort_by': sorting})
932
        if form.is_valid():
933
            sorting = form.cleaned_data.get('sort_by')
934

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

    
949

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

    
1005

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

    
1043

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

    
1063

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

    
1085

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

    
1102

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

    
1119

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

    
1134

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

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

    
1168
    def pluralize(entry):
1169
        entry['plural'] = engine.plural(entry.get('name'))
1170
        return entry
1171

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

    
1184

    
1185
def group_create_list(request):
1186
    form = PickResourceForm()
1187
    return render_response(
1188
        template='im/astakosgroup_create_list.html',
1189
        context_instance=get_context(request),)
1190

    
1191

    
1192
@require_http_methods(["GET"])
1193
@signed_terms_required
1194
@login_required
1195
def billing(request):
1196

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

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

    
1212
    try:
1213
        status, data = r.result
1214
        data = _clear_billing_data(data)
1215
        if status != 200:
1216
            messages.error(request, _('Service response status: %d' % status))
1217
    except:
1218
        messages.error(request, r.result)
1219

    
1220
    print type(start)
1221

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

    
1231

    
1232
def _clear_billing_data(data):
1233

    
1234
    # remove addcredits entries
1235
    def isnotcredit(e):
1236
        return e['serviceName'] != "addcredits"
1237

    
1238
    # separate services
1239
    def servicefilter(service_name):
1240
        service = service_name
1241

    
1242
        def fltr(e):
1243
            return e['serviceName'] == service
1244
        return fltr
1245

    
1246
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1247
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1248
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1249
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1250

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

    
1276
    return render_response(template='im/timeline.html',
1277
                           context_instance=get_context(request),
1278
                           form=form,
1279
                           timeline_header=timeline_header,
1280
                           timeline_body=timeline_body)
1281
    return data