Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (45.8 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
@signed_terms_required
668
@login_required
669
def group_add(request, kind_name='default'):
670
    result = callpoint.list_resources()
671
    resource_catalog = {'resources':defaultdict(defaultdict),
672
                        'groups':defaultdict(list)}
673
    if result.is_success:
674
        for r in result.data:
675
            service = r.get('service', '')
676
            name = r.get('name', '')
677
            group = r.get('group', '')
678
            unit = r.get('unit', '')
679
            fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
680
            resource_catalog['resources'][fullname] = dict(unit=unit)
681
            resource_catalog['groups'][group].append(fullname)
682
        
683
        resource_catalog = dict(resource_catalog)
684
        for k, v in resource_catalog.iteritems():
685
            resource_catalog[k] = dict(v)
686
    else:
687
        messages.error(
688
            request,
689
            'Unable to retrieve system resources: %s' % result.reason
690
    )
691
    
692
    try:
693
        kind = GroupKind.objects.get(name=kind_name)
694
    except:
695
        return HttpResponseBadRequest(_('No such group kind'))
696
    
697
    resource_presentation = {
698
       'compute': {
699
            'help_text':'group compute help text',
700
        },
701
        'storage': {
702
            'help_text':'group storage help text',
703
        },
704
        'pithos+.diskspace': {
705
            'help_text':'resource pithos+.diskspace help text',
706
        },
707
        'cyclades.vm': {
708
            'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
709
        },
710
        'cyclades.disksize': {
711
            'help_text':'resource cyclades.disksize help text',
712
        },
713
        'cyclades.ram': {
714
            'help_text':'resource cyclades.ram help text',
715
        }
716
    }
717

    
718
    post_save_redirect = '/im/group/%(id)s/'
719
    context_processors = None
720
    model, form_class = get_model_and_form_class(
721
        model=None,
722
        form_class=AstakosGroupCreationForm
723
    )
724
    
725
    if request.method == 'POST':
726
        form = form_class(request.POST, request.FILES)
727
        if form.is_valid():
728
            d = form.cleaned_data.copy()
729
            d['owners'] = [request.user]
730
            result = callpoint.create_groups((d,)).next()
731
            if result.is_success:
732
                new_object = result.data[0]
733
                msg = _("The %(verbose_name)s was created successfully.") %\
734
                    {"verbose_name": model._meta.verbose_name}
735
                messages.success(request, msg, fail_silently=True)
736

    
737
#                # send notification
738
#                 try:
739
#                     send_group_creation_notification(
740
#                         template_name='im/group_creation_notification.txt',
741
#                         dictionary={
742
#                             'group': new_object,
743
#                             'owner': request.user,
744
#                             'policies': list(form.cleaned_data['policies']),
745
#                         }
746
#                     )
747
#                 except SendNotificationError, e:
748
#                     messages.error(request, e, fail_silently=True)
749
                return HttpResponseRedirect(post_save_redirect % new_object)
750
            else:
751
                msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
752
                    {"verbose_name": model._meta.verbose_name,
753
                     "reason":result.reason}
754
                messages.error(request, msg, fail_silently=True)
755
    else:
756
        now = datetime.now()
757
        data = {
758
            'kind': kind
759
        }
760
        form = form_class(data)
761

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

    
776
@signed_terms_required
777
@login_required
778
def group_list(request):
779
    none = request.user.astakos_groups.none()
780
    q = AstakosGroup.objects.raw("""
781
        SELECT auth_group.id,
782
        %s AS groupname,
783
        im_groupkind.name AS kindname,
784
        im_astakosgroup.*,
785
        owner.email AS groupowner,
786
        (SELECT COUNT(*) FROM im_membership
787
            WHERE group_id = im_astakosgroup.group_ptr_id
788
            AND date_joined IS NOT NULL) AS approved_members_num,
789
        (SELECT CASE WHEN(
790
                    SELECT date_joined FROM im_membership
791
                    WHERE group_id = im_astakosgroup.group_ptr_id
792
                    AND person_id = %s) IS NULL
793
                    THEN 0 ELSE 1 END) AS membership_status
794
        FROM im_astakosgroup
795
        INNER JOIN im_membership ON (
796
            im_astakosgroup.group_ptr_id = im_membership.group_id)
797
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
798
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
799
        LEFT JOIN im_astakosuser_owner ON (
800
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
801
        LEFT JOIN auth_user as owner ON (
802
            im_astakosuser_owner.astakosuser_id = owner.id)
803
        WHERE im_membership.person_id = %s
804
        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
805
    d = defaultdict(list)
806
    for g in q:
807
        if request.user.email == g.groupowner:
808
            d['own'].append(g)
809
        else:
810
            d['other'].append(g)
811

    
812
    # validate sorting
813
    fields = ('own', 'other')
814
    for f in fields:
815
        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
816
        if v:
817
            form = AstakosGroupSortForm({'sort_by': v})
818
            if not form.is_valid():
819
                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
820
    return object_list(request, queryset=none,
821
                       extra_context={'is_search': False,
822
                                      'mine': d['own'],
823
                                      'other': d['other'],
824
                                      'own_sorting': own_sorting,
825
                                      'other_sorting': other_sorting,
826
                                      'own_page': request.GET.get('own_page', 1),
827
                                      'other_page': request.GET.get('other_page', 1)
828
                                      })
829

    
830

    
831
@signed_terms_required
832
@login_required
833
def group_detail(request, group_id):
834
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
835
    q = q.extra(select={
836
        'is_member': """SELECT CASE WHEN EXISTS(
837
                            SELECT id FROM im_membership
838
                            WHERE group_id = im_astakosgroup.group_ptr_id
839
                            AND person_id = %s)
840
                        THEN 1 ELSE 0 END""" % request.user.id,
841
        'is_owner': """SELECT CASE WHEN EXISTS(
842
                        SELECT id FROM im_astakosuser_owner
843
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
844
                        AND astakosuser_id = %s)
845
                        THEN 1 ELSE 0 END""" % request.user.id,
846
        'kindname': """SELECT name FROM im_groupkind
847
                       WHERE id = im_astakosgroup.kind_id"""})
848

    
849
    model = q.model
850
    context_processors = None
851
    mimetype = None
852
    try:
853
        obj = q.get()
854
    except AstakosGroup.DoesNotExist:
855
        raise Http404("No %s found matching the query" % (
856
            model._meta.verbose_name))
857

    
858
    update_form = AstakosGroupUpdateForm(instance=obj)
859
    addmembers_form = AddGroupMembersForm()
860
    if request.method == 'POST':
861
        update_data = {}
862
        addmembers_data = {}
863
        for k, v in request.POST.iteritems():
864
            if k in update_form.fields:
865
                update_data[k] = v
866
            if k in addmembers_form.fields:
867
                addmembers_data[k] = v
868
        update_data = update_data or None
869
        addmembers_data = addmembers_data or None
870
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
871
        addmembers_form = AddGroupMembersForm(addmembers_data)
872
        if update_form.is_valid():
873
            update_form.save()
874
        if addmembers_form.is_valid():
875
            map(obj.approve_member, addmembers_form.valid_users)
876
            addmembers_form = AddGroupMembersForm()
877

    
878
    template_name = "%s/%s_detail.html" % (
879
        model._meta.app_label, model._meta.object_name.lower())
880
    t = template_loader.get_template(template_name)
881
    c = RequestContext(request, {
882
        'object': obj,
883
    }, context_processors)
884

    
885
    # validate sorting
886
    sorting = request.GET.get('sorting')
887
    if sorting:
888
        form = MembersSortForm({'sort_by': sorting})
889
        if form.is_valid():
890
            sorting = form.cleaned_data.get('sort_by')
891

    
892
    extra_context = {'update_form': update_form,
893
                     'addmembers_form': addmembers_form,
894
                     'page': request.GET.get('page', 1),
895
                     'sorting': sorting}
896
    for key, value in extra_context.items():
897
        if callable(value):
898
            c[key] = value()
899
        else:
900
            c[key] = value
901
    response = HttpResponse(t.render(c), mimetype=mimetype)
902
    populate_xheaders(
903
        request, response, model, getattr(obj, obj._meta.pk.name))
904
    return response
905

    
906

    
907
@signed_terms_required
908
@login_required
909
def group_search(request, extra_context=None, **kwargs):
910
    q = request.GET.get('q')
911
    sorting = request.GET.get('sorting')
912
    if request.method == 'GET':
913
        form = AstakosGroupSearchForm({'q': q} if q else None)
914
    else:
915
        form = AstakosGroupSearchForm(get_query(request))
916
        if form.is_valid():
917
            q = form.cleaned_data['q'].strip()
918
    if q:
919
        queryset = AstakosGroup.objects.select_related()
920
        queryset = queryset.filter(name__contains=q)
921
        queryset = queryset.filter(approval_date__isnull=False)
922
        queryset = queryset.extra(select={
923
                                  'groupname': DB_REPLACE_GROUP_SCHEME,
924
                                  'kindname': "im_groupkind.name",
925
                                  'approved_members_num': """
926
                    SELECT COUNT(*) FROM im_membership
927
                    WHERE group_id = im_astakosgroup.group_ptr_id
928
                    AND date_joined IS NOT NULL""",
929
                                  'membership_approval_date': """
930
                    SELECT date_joined FROM im_membership
931
                    WHERE group_id = im_astakosgroup.group_ptr_id
932
                    AND person_id = %s""" % request.user.id,
933
                                  'is_member': """
934
                    SELECT CASE WHEN EXISTS(
935
                    SELECT date_joined FROM im_membership
936
                    WHERE group_id = im_astakosgroup.group_ptr_id
937
                    AND person_id = %s)
938
                    THEN 1 ELSE 0 END""" % request.user.id,
939
                                  'is_owner': """
940
                    SELECT CASE WHEN EXISTS(
941
                    SELECT id FROM im_astakosuser_owner
942
                    WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
943
                    AND astakosuser_id = %s)
944
                    THEN 1 ELSE 0 END""" % request.user.id})
945
        if sorting:
946
            # TODO check sorting value
947
            queryset = queryset.order_by(sorting)
948
    else:
949
        queryset = AstakosGroup.objects.none()
950
    return object_list(
951
        request,
952
        queryset,
953
        paginate_by=PAGINATE_BY,
954
        page=request.GET.get('page') or 1,
955
        template_name='im/astakosgroup_list.html',
956
        extra_context=dict(form=form,
957
                           is_search=True,
958
                           q=q,
959
                           sorting=sorting))
960

    
961

    
962
@signed_terms_required
963
@login_required
964
def group_all(request, extra_context=None, **kwargs):
965
    q = AstakosGroup.objects.select_related()
966
    q = q.filter(approval_date__isnull=False)
967
    q = q.extra(select={
968
                'groupname': DB_REPLACE_GROUP_SCHEME,
969
                'kindname': "im_groupkind.name",
970
                'approved_members_num': """
971
                    SELECT COUNT(*) FROM im_membership
972
                    WHERE group_id = im_astakosgroup.group_ptr_id
973
                    AND date_joined IS NOT NULL""",
974
                'membership_approval_date': """
975
                    SELECT date_joined FROM im_membership
976
                    WHERE group_id = im_astakosgroup.group_ptr_id
977
                    AND person_id = %s""" % request.user.id,
978
                'is_member': """
979
                    SELECT CASE WHEN EXISTS(
980
                    SELECT date_joined FROM im_membership
981
                    WHERE group_id = im_astakosgroup.group_ptr_id
982
                    AND person_id = %s)
983
                    THEN 1 ELSE 0 END""" % request.user.id})
984
    sorting = request.GET.get('sorting')
985
    if sorting:
986
        # TODO check sorting value
987
        q = q.order_by(sorting)
988
    return object_list(
989
        request,
990
        q,
991
        paginate_by=PAGINATE_BY,
992
        page=request.GET.get('page') or 1,
993
        template_name='im/astakosgroup_list.html',
994
        extra_context=dict(form=AstakosGroupSearchForm(),
995
                           is_search=True,
996
                           sorting=sorting))
997

    
998

    
999
@signed_terms_required
1000
@login_required
1001
def group_join(request, group_id):
1002
    m = Membership(group_id=group_id,
1003
                   person=request.user,
1004
                   date_requested=datetime.now())
1005
    try:
1006
        m.save()
1007
        post_save_redirect = reverse(
1008
            'group_detail',
1009
            kwargs=dict(group_id=group_id))
1010
        return HttpResponseRedirect(post_save_redirect)
1011
    except IntegrityError, e:
1012
        logger.exception(e)
1013
        msg = _('Failed to join group.')
1014
        messages.error(request, msg)
1015
        return group_search(request)
1016

    
1017

    
1018
@signed_terms_required
1019
@login_required
1020
def group_leave(request, group_id):
1021
    try:
1022
        m = Membership.objects.select_related().get(
1023
            group__id=group_id,
1024
            person=request.user)
1025
    except Membership.DoesNotExist:
1026
        return HttpResponseBadRequest(_('Invalid membership.'))
1027
    if request.user in m.group.owner.all():
1028
        return HttpResponseForbidden(_('Owner can not leave the group.'))
1029
    return delete_object(
1030
        request,
1031
        model=Membership,
1032
        object_id=m.id,
1033
        template_name='im/astakosgroup_list.html',
1034
        post_delete_redirect=reverse(
1035
            'group_detail',
1036
            kwargs=dict(group_id=group_id)))
1037

    
1038

    
1039
def handle_membership(func):
1040
    @wraps(func)
1041
    def wrapper(request, group_id, user_id):
1042
        try:
1043
            m = Membership.objects.select_related().get(
1044
                group__id=group_id,
1045
                person__id=user_id)
1046
        except Membership.DoesNotExist:
1047
            return HttpResponseBadRequest(_('Invalid membership.'))
1048
        else:
1049
            if request.user not in m.group.owner.all():
1050
                return HttpResponseForbidden(_('User is not a group owner.'))
1051
            func(request, m)
1052
            return group_detail(request, group_id)
1053
    return wrapper
1054

    
1055

    
1056
@signed_terms_required
1057
@login_required
1058
@handle_membership
1059
def approve_member(request, membership):
1060
    try:
1061
        membership.approve()
1062
        realname = membership.person.realname
1063
        msg = _('%s has been successfully joined the group.' % realname)
1064
        messages.success(request, msg)
1065
    except BaseException, e:
1066
        logger.exception(e)
1067
        realname = membership.person.realname
1068
        msg = _('Something went wrong during %s\'s approval.' % realname)
1069
        messages.error(request, msg)
1070

    
1071

    
1072
@signed_terms_required
1073
@login_required
1074
@handle_membership
1075
def disapprove_member(request, membership):
1076
    try:
1077
        membership.disapprove()
1078
        realname = membership.person.realname
1079
        msg = _('%s has been successfully removed from the group.' % realname)
1080
        messages.success(request, msg)
1081
    except BaseException, e:
1082
        logger.exception(e)
1083
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
1084
        messages.error(request, msg)
1085

    
1086

    
1087
@signed_terms_required
1088
@login_required
1089
def resource_list(request):
1090
#     if request.method == 'POST':
1091
#         form = PickResourceForm(request.POST)
1092
#         if form.is_valid():
1093
#             r = form.cleaned_data.get('resource')
1094
#             if r:
1095
#                 groups = request.user.membership_set.only('group').filter(
1096
#                     date_joined__isnull=False)
1097
#                 groups = [g.group_id for g in groups]
1098
#                 q = AstakosGroupQuota.objects.select_related().filter(
1099
#                     resource=r, group__in=groups)
1100
#     else:
1101
#         form = PickResourceForm()
1102
#         q = AstakosGroupQuota.objects.none()
1103
#
1104
#     return object_list(request, q,
1105
#                        template_name='im/astakosuserquota_list.html',
1106
#                        extra_context={'form': form, 'data':data})
1107

    
1108
    def with_class(entry):
1109
        entry['load_class'] = 'red'
1110
        max_value = float(entry['maxValue'])
1111
        curr_value = float(entry['currValue'])
1112
        entry['ratio'] = (curr_value / max_value) * 100
1113
        if entry['ratio'] < 66:
1114
            entry['load_class'] = 'yellow'
1115
        if entry['ratio'] < 33:
1116
            entry['load_class'] = 'green'
1117
        return entry
1118

    
1119
    def pluralize(entry):
1120
        entry['plural'] = engine.plural(entry.get('name'))
1121
        return entry
1122

    
1123
    result = callpoint.get_user_status(request.user.id)
1124
    if result.is_success:
1125
        backenddata = map(with_class, result.data)
1126
        data = map(pluralize, result.data)
1127
    else:
1128
        data = None
1129
        messages.error(request, result.reason)
1130
    return render_response('im/resource_list.html',
1131
                           data=data,
1132
                           context_instance=get_context(request))
1133

    
1134

    
1135
def group_create_list(request):
1136
    form = PickResourceForm()
1137
    return render_response(
1138
        template='im/astakosgroup_create_list.html',
1139
        context_instance=get_context(request),)
1140

    
1141

    
1142
@signed_terms_required
1143
@login_required
1144
def billing(request):
1145

    
1146
    today = datetime.today()
1147
    month_last_day = calendar.monthrange(today.year, today.month)[1]
1148
    data['resources'] = map(with_class, data['resources'])
1149
    start = request.POST.get('datefrom', None)
1150
    if start:
1151
        today = datetime.fromtimestamp(int(start))
1152
        month_last_day = calendar.monthrange(today.year, today.month)[1]
1153

    
1154
    start = datetime(today.year, today.month, 1).strftime("%s")
1155
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1156
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1157
                                    int(start) * 1000,
1158
                                    int(end) * 1000))
1159
    data = {}
1160

    
1161
    try:
1162
        status, data = r.result
1163
        data = _clear_billing_data(data)
1164
        if status != 200:
1165
            messages.error(request, _('Service response status: %d' % status))
1166
    except:
1167
        messages.error(request, r.result)
1168

    
1169
    print type(start)
1170

    
1171
    return render_response(
1172
        template='im/billing.html',
1173
        context_instance=get_context(request),
1174
        data=data,
1175
        zerodate=datetime(month=1, year=1970, day=1),
1176
        today=today,
1177
        start=int(start),
1178
        month_last_day=month_last_day)
1179

    
1180

    
1181
def _clear_billing_data(data):
1182

    
1183
    # remove addcredits entries
1184
    def isnotcredit(e):
1185
        return e['serviceName'] != "addcredits"
1186

    
1187
    # separate services
1188
    def servicefilter(service_name):
1189
        service = service_name
1190

    
1191
        def fltr(e):
1192
            return e['serviceName'] == service
1193
        return fltr
1194

    
1195
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1196
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1197
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1198
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1199

    
1200
    return data
1201
     
1202
     
1203
@signed_terms_required
1204
@login_required
1205
def timeline(request):
1206
#    data = {'entity':request.user.email}
1207
    timeline_body = ()
1208
    timeline_header = ()
1209
#    form = TimelineForm(data)
1210
    form = TimelineForm()
1211
    if request.method == 'POST':
1212
        data = request.POST
1213
        form = TimelineForm(data)
1214
        if form.is_valid():
1215
            data = form.cleaned_data
1216
            timeline_header = ('entity', 'resource',
1217
                               'event name', 'event date',
1218
                               'incremental cost', 'total cost')
1219
            timeline_body = timeline_charge(
1220
                data['entity'], data['resource'],
1221
                data['start_date'], data['end_date'],
1222
                data['details'], data['operation'])
1223

    
1224
    return render_response(template='im/timeline.html',
1225
                           context_instance=get_context(request),
1226
                           form=form,
1227
                           timeline_header=timeline_header,
1228
                           timeline_body=timeline_body)
1229
    return data 
1230

    
1231

    
1232
def group_summary(request):
1233
    return render_response(
1234
        template='im/astakosgroup_form_summary.html',
1235
        context_instance=get_context(request)  )