Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 8c804c12

History | View | Annotate | Download (46.4 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

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

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

    
71
from astakos.im.activation_backends import get_backend, SimpleBackend
72
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
73
from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
74
                              FeedbackForm, SignApprovalTermsForm,
75
                              ExtendedPasswordChangeForm, EmailChangeForm,
76
                              AstakosGroupCreationForm, AstakosGroupSearchForm,
77
                              AstakosGroupUpdateForm, AddGroupMembersForm,
78
                              AstakosGroupSortForm, MembersSortForm,
79
                              TimelineForm, PickResourceForm)
80
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
@require_http_methods(["GET", "POST"])
668
@signed_terms_required
669
@login_required
670
def group_add(request, kind_name='default'):
671
    result = callpoint.list_resources()
672
    resource_catalog = {'resources':defaultdict(defaultdict),
673
                        'groups':defaultdict(list)}
674
    if result.is_success:
675
        for r in result.data:
676
            service = r.get('service', '')
677
            name = r.get('name', '')
678
            group = r.get('group', '')
679
            unit = r.get('unit', '')
680
            fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
681
            resource_catalog['resources'][fullname] = dict(unit=unit)
682
            resource_catalog['groups'][group].append(fullname)
683
        
684
        resource_catalog = dict(resource_catalog)
685
        for k, v in resource_catalog.iteritems():
686
            resource_catalog[k] = dict(v)
687
    else:
688
        messages.error(
689
            request,
690
            'Unable to retrieve system resources: %s' % result.reason
691
    )
692
    
693
    try:
694
        kind = GroupKind.objects.get(name=kind_name)
695
    except:
696
        return HttpResponseBadRequest(_('No such group kind'))
697
    
698
    resource_presentation = {
699
       'compute': {
700
            'help_text':'group compute help text',
701
        },
702
        'storage': {
703
            'help_text':'group storage help text',
704
        },
705
        'pithos+.diskspace': {
706
            'help_text':'resource pithos+.diskspace help text',
707
        },
708
        'cyclades.vm': {
709
            'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
710
        },
711
        'cyclades.disksize': {
712
            'help_text':'resource cyclades.disksize help text',
713
        },
714
        'cyclades.ram': {
715
            'help_text':'resource cyclades.ram help text',
716
        }
717
    }
718

    
719
    post_save_redirect = '/im/group/%(id)s/'
720
    context_processors = None
721
    model, form_class = get_model_and_form_class(
722
        model=None,
723
        form_class=AstakosGroupCreationForm
724
    )
725
    
726
    if request.method == 'POST':
727
        form = form_class(request.POST, request.FILES)
728
        if form.is_valid():
729
            return render_response(
730
                template='im/astakosgroup_form_summary.html',
731
                context_instance=get_context(request),
732
                data=form.cleaned_data
733
            )
734
    else:
735
        now = datetime.now()
736
        data = {
737
            'kind': kind
738
        }
739
        form = form_class(data)
740

    
741
    # Create the template, context, response
742
    template_name = "%s/%s_form_demo.html" % (
743
        model._meta.app_label,
744
        model._meta.object_name.lower()
745
    )
746
    t = template_loader.get_template(template_name)
747
    c = RequestContext(request, {
748
        'form': form,
749
        'kind': kind,
750
        'resource_catalog':resource_catalog,
751
        'resource_presentation':resource_presentation,
752
    }, context_processors)
753
    return HttpResponse(t.render(c))
754

    
755

    
756
# @require_http_methods(["POST"])
757
# @signed_terms_required
758
# @login_required
759
def group_add_complete(request):
760
    d = dict(request.POST)
761
    d['owners'] = [request.user]
762
    result = callpoint.create_groups((d,)).next()
763
    if result.is_success:
764
        new_object = result.data[0]
765
        model = AstakosGroup
766
        msg = _("The %(verbose_name)s was created successfully.") %\
767
            {"verbose_name": model._meta.verbose_name}
768
        messages.success(request, msg, fail_silently=True)
769

    
770
#                # send notification
771
#                 try:
772
#                     send_group_creation_notification(
773
#                         template_name='im/group_creation_notification.txt',
774
#                         dictionary={
775
#                             'group': new_object,
776
#                             'owner': request.user,
777
#                             'policies': list(form.cleaned_data['policies']),
778
#                         }
779
#                     )
780
#                 except SendNotificationError, e:
781
#                     messages.error(request, e, fail_silently=True)
782
        post_save_redirect = '/im/group/%(id)s/'
783
        return HttpResponseRedirect(post_save_redirect % new_object)
784
    else:
785
        msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
786
            {"verbose_name": model._meta.verbose_name,
787
             "reason":result.reason}
788
        messages.error(request, msg, fail_silently=True)
789
    
790
    return render_response(
791
    template='im/astakosgroup_form_summary.html',
792
    context_instance=get_context(request))
793

    
794

    
795
@require_http_methods(["GET"])
796
@signed_terms_required
797
@login_required
798
def group_list(request):
799
    none = request.user.astakos_groups.none()
800
    q = AstakosGroup.objects.raw("""
801
        SELECT auth_group.id,
802
        %s AS groupname,
803
        im_groupkind.name AS kindname,
804
        im_astakosgroup.*,
805
        owner.email AS groupowner,
806
        (SELECT COUNT(*) FROM im_membership
807
            WHERE group_id = im_astakosgroup.group_ptr_id
808
            AND date_joined IS NOT NULL) AS approved_members_num,
809
        (SELECT CASE WHEN(
810
                    SELECT date_joined FROM im_membership
811
                    WHERE group_id = im_astakosgroup.group_ptr_id
812
                    AND person_id = %s) IS NULL
813
                    THEN 0 ELSE 1 END) AS membership_status
814
        FROM im_astakosgroup
815
        INNER JOIN im_membership ON (
816
            im_astakosgroup.group_ptr_id = im_membership.group_id)
817
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
818
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
819
        LEFT JOIN im_astakosuser_owner ON (
820
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
821
        LEFT JOIN auth_user as owner ON (
822
            im_astakosuser_owner.astakosuser_id = owner.id)
823
        WHERE im_membership.person_id = %s
824
        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
825
    d = defaultdict(list)
826
    for g in q:
827
        if request.user.email == g.groupowner:
828
            d['own'].append(g)
829
        else:
830
            d['other'].append(g)
831

    
832
    # validate sorting
833
    fields = ('own', 'other')
834
    for f in fields:
835
        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
836
        if v:
837
            form = AstakosGroupSortForm({'sort_by': v})
838
            if not form.is_valid():
839
                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
840
    return object_list(request, queryset=none,
841
                       extra_context={'is_search': False,
842
                                      'mine': d['own'],
843
                                      'other': d['other'],
844
                                      'own_sorting': own_sorting,
845
                                      'other_sorting': other_sorting,
846
                                      'own_page': request.GET.get('own_page', 1),
847
                                      'other_page': request.GET.get('other_page', 1)
848
                                      })
849

    
850

    
851
@require_http_methods(["GET", "POST"])
852
@signed_terms_required
853
@login_required
854
def group_detail(request, group_id):
855
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
856
    q = q.extra(select={
857
        'is_member': """SELECT CASE WHEN EXISTS(
858
                            SELECT id FROM im_membership
859
                            WHERE group_id = im_astakosgroup.group_ptr_id
860
                            AND person_id = %s)
861
                        THEN 1 ELSE 0 END""" % request.user.id,
862
        'is_owner': """SELECT CASE WHEN EXISTS(
863
                        SELECT id FROM im_astakosuser_owner
864
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
865
                        AND astakosuser_id = %s)
866
                        THEN 1 ELSE 0 END""" % request.user.id,
867
        'kindname': """SELECT name FROM im_groupkind
868
                       WHERE id = im_astakosgroup.kind_id"""})
869

    
870
    model = q.model
871
    context_processors = None
872
    mimetype = None
873
    try:
874
        obj = q.get()
875
    except AstakosGroup.DoesNotExist:
876
        raise Http404("No %s found matching the query" % (
877
            model._meta.verbose_name))
878

    
879
    update_form = AstakosGroupUpdateForm(instance=obj)
880
    addmembers_form = AddGroupMembersForm()
881
    if request.method == 'POST':
882
        update_data = {}
883
        addmembers_data = {}
884
        for k, v in request.POST.iteritems():
885
            if k in update_form.fields:
886
                update_data[k] = v
887
            if k in addmembers_form.fields:
888
                addmembers_data[k] = v
889
        update_data = update_data or None
890
        addmembers_data = addmembers_data or None
891
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
892
        addmembers_form = AddGroupMembersForm(addmembers_data)
893
        if update_form.is_valid():
894
            update_form.save()
895
        if addmembers_form.is_valid():
896
            map(obj.approve_member, addmembers_form.valid_users)
897
            addmembers_form = AddGroupMembersForm()
898

    
899
    template_name = "%s/%s_detail.html" % (
900
        model._meta.app_label, model._meta.object_name.lower())
901
    t = template_loader.get_template(template_name)
902
    c = RequestContext(request, {
903
        'object': obj,
904
    }, context_processors)
905

    
906
    # validate sorting
907
    sorting = request.GET.get('sorting')
908
    if sorting:
909
        form = MembersSortForm({'sort_by': sorting})
910
        if form.is_valid():
911
            sorting = form.cleaned_data.get('sort_by')
912

    
913
    extra_context = {'update_form': update_form,
914
                     'addmembers_form': addmembers_form,
915
                     'page': request.GET.get('page', 1),
916
                     'sorting': sorting}
917
    for key, value in extra_context.items():
918
        if callable(value):
919
            c[key] = value()
920
        else:
921
            c[key] = value
922
    response = HttpResponse(t.render(c), mimetype=mimetype)
923
    populate_xheaders(
924
        request, response, model, getattr(obj, obj._meta.pk.name))
925
    return response
926

    
927

    
928
@require_http_methods(["GET", "POST"])
929
@signed_terms_required
930
@login_required
931
def group_search(request, extra_context=None, **kwargs):
932
    q = request.GET.get('q')
933
    sorting = request.GET.get('sorting')
934
    if request.method == 'GET':
935
        form = AstakosGroupSearchForm({'q': q} if q else None)
936
    else:
937
        form = AstakosGroupSearchForm(get_query(request))
938
        if form.is_valid():
939
            q = form.cleaned_data['q'].strip()
940
    if q:
941
        queryset = AstakosGroup.objects.select_related()
942
        queryset = queryset.filter(name__contains=q)
943
        queryset = queryset.filter(approval_date__isnull=False)
944
        queryset = queryset.extra(select={
945
                                  'groupname': DB_REPLACE_GROUP_SCHEME,
946
                                  'kindname': "im_groupkind.name",
947
                                  'approved_members_num': """
948
                    SELECT COUNT(*) FROM im_membership
949
                    WHERE group_id = im_astakosgroup.group_ptr_id
950
                    AND date_joined IS NOT NULL""",
951
                                  'membership_approval_date': """
952
                    SELECT date_joined FROM im_membership
953
                    WHERE group_id = im_astakosgroup.group_ptr_id
954
                    AND person_id = %s""" % request.user.id,
955
                                  'is_member': """
956
                    SELECT CASE WHEN EXISTS(
957
                    SELECT date_joined FROM im_membership
958
                    WHERE group_id = im_astakosgroup.group_ptr_id
959
                    AND person_id = %s)
960
                    THEN 1 ELSE 0 END""" % request.user.id,
961
                                  'is_owner': """
962
                    SELECT CASE WHEN EXISTS(
963
                    SELECT id FROM im_astakosuser_owner
964
                    WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
965
                    AND astakosuser_id = %s)
966
                    THEN 1 ELSE 0 END""" % request.user.id})
967
        if sorting:
968
            # TODO check sorting value
969
            queryset = queryset.order_by(sorting)
970
    else:
971
        queryset = AstakosGroup.objects.none()
972
    return object_list(
973
        request,
974
        queryset,
975
        paginate_by=PAGINATE_BY,
976
        page=request.GET.get('page') or 1,
977
        template_name='im/astakosgroup_list.html',
978
        extra_context=dict(form=form,
979
                           is_search=True,
980
                           q=q,
981
                           sorting=sorting))
982

    
983

    
984
@require_http_methods(["GET"])
985
@signed_terms_required
986
@login_required
987
def group_all(request, extra_context=None, **kwargs):
988
    q = AstakosGroup.objects.select_related()
989
    q = q.filter(approval_date__isnull=False)
990
    q = q.extra(select={
991
                'groupname': DB_REPLACE_GROUP_SCHEME,
992
                'kindname': "im_groupkind.name",
993
                'approved_members_num': """
994
                    SELECT COUNT(*) FROM im_membership
995
                    WHERE group_id = im_astakosgroup.group_ptr_id
996
                    AND date_joined IS NOT NULL""",
997
                'membership_approval_date': """
998
                    SELECT date_joined FROM im_membership
999
                    WHERE group_id = im_astakosgroup.group_ptr_id
1000
                    AND person_id = %s""" % request.user.id,
1001
                'is_member': """
1002
                    SELECT CASE WHEN EXISTS(
1003
                    SELECT date_joined FROM im_membership
1004
                    WHERE group_id = im_astakosgroup.group_ptr_id
1005
                    AND person_id = %s)
1006
                    THEN 1 ELSE 0 END""" % request.user.id})
1007
    sorting = request.GET.get('sorting')
1008
    if sorting:
1009
        # TODO check sorting value
1010
        q = q.order_by(sorting)
1011
    return object_list(
1012
        request,
1013
        q,
1014
        paginate_by=PAGINATE_BY,
1015
        page=request.GET.get('page') or 1,
1016
        template_name='im/astakosgroup_list.html',
1017
        extra_context=dict(form=AstakosGroupSearchForm(),
1018
                           is_search=True,
1019
                           sorting=sorting))
1020

    
1021

    
1022
@require_http_methods(["POST"])
1023
@signed_terms_required
1024
@login_required
1025
def group_join(request, group_id):
1026
    m = Membership(group_id=group_id,
1027
                   person=request.user,
1028
                   date_requested=datetime.now())
1029
    try:
1030
        m.save()
1031
        post_save_redirect = reverse(
1032
            'group_detail',
1033
            kwargs=dict(group_id=group_id))
1034
        return HttpResponseRedirect(post_save_redirect)
1035
    except IntegrityError, e:
1036
        logger.exception(e)
1037
        msg = _('Failed to join group.')
1038
        messages.error(request, msg)
1039
        return group_search(request)
1040

    
1041

    
1042
@require_http_methods(["POST"])
1043
@signed_terms_required
1044
@login_required
1045
def group_leave(request, group_id):
1046
    try:
1047
        m = Membership.objects.select_related().get(
1048
            group__id=group_id,
1049
            person=request.user)
1050
    except Membership.DoesNotExist:
1051
        return HttpResponseBadRequest(_('Invalid membership.'))
1052
    if request.user in m.group.owner.all():
1053
        return HttpResponseForbidden(_('Owner can not leave the group.'))
1054
    return delete_object(
1055
        request,
1056
        model=Membership,
1057
        object_id=m.id,
1058
        template_name='im/astakosgroup_list.html',
1059
        post_delete_redirect=reverse(
1060
            'group_detail',
1061
            kwargs=dict(group_id=group_id)))
1062

    
1063

    
1064
def handle_membership(func):
1065
    @wraps(func)
1066
    def wrapper(request, group_id, user_id):
1067
        try:
1068
            m = Membership.objects.select_related().get(
1069
                group__id=group_id,
1070
                person__id=user_id)
1071
        except Membership.DoesNotExist:
1072
            return HttpResponseBadRequest(_('Invalid membership.'))
1073
        else:
1074
            if request.user not in m.group.owner.all():
1075
                return HttpResponseForbidden(_('User is not a group owner.'))
1076
            func(request, m)
1077
            return group_detail(request, group_id)
1078
    return wrapper
1079

    
1080

    
1081
@require_http_methods(["POST"])
1082
@signed_terms_required
1083
@login_required
1084
@handle_membership
1085
def approve_member(request, membership):
1086
    try:
1087
        membership.approve()
1088
        realname = membership.person.realname
1089
        msg = _('%s has been successfully joined the group.' % realname)
1090
        messages.success(request, msg)
1091
    except BaseException, e:
1092
        logger.exception(e)
1093
        realname = membership.person.realname
1094
        msg = _('Something went wrong during %s\'s approval.' % realname)
1095
        messages.error(request, msg)
1096

    
1097

    
1098
@signed_terms_required
1099
@login_required
1100
@handle_membership
1101
def disapprove_member(request, membership):
1102
    try:
1103
        membership.disapprove()
1104
        realname = membership.person.realname
1105
        msg = _('%s has been successfully removed from the group.' % realname)
1106
        messages.success(request, msg)
1107
    except BaseException, e:
1108
        logger.exception(e)
1109
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
1110
        messages.error(request, msg)
1111

    
1112

    
1113
@require_http_methods(["GET"])
1114
@signed_terms_required
1115
@login_required
1116
def resource_list(request):
1117
#     if request.method == 'POST':
1118
#         form = PickResourceForm(request.POST)
1119
#         if form.is_valid():
1120
#             r = form.cleaned_data.get('resource')
1121
#             if r:
1122
#                 groups = request.user.membership_set.only('group').filter(
1123
#                     date_joined__isnull=False)
1124
#                 groups = [g.group_id for g in groups]
1125
#                 q = AstakosGroupQuota.objects.select_related().filter(
1126
#                     resource=r, group__in=groups)
1127
#     else:
1128
#         form = PickResourceForm()
1129
#         q = AstakosGroupQuota.objects.none()
1130
#
1131
#     return object_list(request, q,
1132
#                        template_name='im/astakosuserquota_list.html',
1133
#                        extra_context={'form': form, 'data':data})
1134

    
1135
    def with_class(entry):
1136
        entry['load_class'] = 'red'
1137
        max_value = float(entry['maxValue'])
1138
        curr_value = float(entry['currValue'])
1139
        entry['ratio'] = (curr_value / max_value) * 100
1140
        if entry['ratio'] < 66:
1141
            entry['load_class'] = 'yellow'
1142
        if entry['ratio'] < 33:
1143
            entry['load_class'] = 'green'
1144
        return entry
1145

    
1146
    def pluralize(entry):
1147
        entry['plural'] = engine.plural(entry.get('name'))
1148
        return entry
1149

    
1150
    result = callpoint.get_user_status(request.user.id)
1151
    if result.is_success:
1152
        backenddata = map(with_class, result.data)
1153
        data = map(pluralize, result.data)
1154
    else:
1155
        data = None
1156
        messages.error(request, result.reason)
1157
    return render_response('im/resource_list.html',
1158
                           data=data,
1159
                           context_instance=get_context(request))
1160

    
1161

    
1162
def group_create_list(request):
1163
    form = PickResourceForm()
1164
    return render_response(
1165
        template='im/astakosgroup_create_list.html',
1166
        context_instance=get_context(request),)
1167

    
1168

    
1169
@require_http_methods(["GET"])
1170
@signed_terms_required
1171
@login_required
1172
def billing(request):
1173

    
1174
    today = datetime.today()
1175
    month_last_day = calendar.monthrange(today.year, today.month)[1]
1176
    data['resources'] = map(with_class, data['resources'])
1177
    start = request.POST.get('datefrom', None)
1178
    if start:
1179
        today = datetime.fromtimestamp(int(start))
1180
        month_last_day = calendar.monthrange(today.year, today.month)[1]
1181

    
1182
    start = datetime(today.year, today.month, 1).strftime("%s")
1183
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1184
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1185
                                    int(start) * 1000,
1186
                                    int(end) * 1000))
1187
    data = {}
1188

    
1189
    try:
1190
        status, data = r.result
1191
        data = _clear_billing_data(data)
1192
        if status != 200:
1193
            messages.error(request, _('Service response status: %d' % status))
1194
    except:
1195
        messages.error(request, r.result)
1196

    
1197
    print type(start)
1198

    
1199
    return render_response(
1200
        template='im/billing.html',
1201
        context_instance=get_context(request),
1202
        data=data,
1203
        zerodate=datetime(month=1, year=1970, day=1),
1204
        today=today,
1205
        start=int(start),
1206
        month_last_day=month_last_day)
1207

    
1208

    
1209
def _clear_billing_data(data):
1210

    
1211
    # remove addcredits entries
1212
    def isnotcredit(e):
1213
        return e['serviceName'] != "addcredits"
1214

    
1215
    # separate services
1216
    def servicefilter(service_name):
1217
        service = service_name
1218

    
1219
        def fltr(e):
1220
            return e['serviceName'] == service
1221
        return fltr
1222

    
1223
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1224
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1225
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1226
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1227

    
1228
    return data
1229
     
1230
     
1231
@require_http_methods(["GET"])
1232
@signed_terms_required
1233
@login_required
1234
def timeline(request):
1235
#    data = {'entity':request.user.email}
1236
    timeline_body = ()
1237
    timeline_header = ()
1238
#    form = TimelineForm(data)
1239
    form = TimelineForm()
1240
    if request.method == 'POST':
1241
        data = request.POST
1242
        form = TimelineForm(data)
1243
        if form.is_valid():
1244
            data = form.cleaned_data
1245
            timeline_header = ('entity', 'resource',
1246
                               'event name', 'event date',
1247
                               'incremental cost', 'total cost')
1248
            timeline_body = timeline_charge(
1249
                data['entity'], data['resource'],
1250
                data['start_date'], data['end_date'],
1251
                data['details'], data['operation'])
1252

    
1253
    return render_response(template='im/timeline.html',
1254
                           context_instance=get_context(request),
1255
                           form=form,
1256
                           timeline_header=timeline_header,
1257
                           timeline_body=timeline_body)
1258
    return data