Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (46.7 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 AstakosDjangoDBCallpoint
91

    
92
logger = logging.getLogger(__name__)
93

    
94

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

    
98
callpoint = AstakosDjangoDBCallpoint()
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
@signed_terms_required
150
def index(request, login_template_name='im/login.html', extra_context=None):
151
    """
152
    If there is logged on user renders the profile page otherwise renders login page.
153

154
    **Arguments**
155

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

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

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

167
    **Template:**
168

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

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

    
179

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

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

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

194
    If the user isn't logged in, redirects to settings.LOGIN_URL.
195

196
    **Arguments**
197

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

202
    ``extra_context``
203
        An dictionary of variables to add to the template context.
204

205
    **Template:**
206

207
    im/invitations.html or ``template_name`` keyword argument.
208

209
    **Settings:**
210

211
    The view expectes the following settings are defined:
212

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

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

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

    
257

    
258
@login_required
259
@signed_terms_required
260
def edit_profile(request, template_name='im/profile.html', extra_context=None):
261
    """
262
    Allows a user to edit his/her profile.
263

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

268
    If the user isn't logged in, redirects to settings.LOGIN_URL.
269

270
    **Arguments**
271

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

276
    ``extra_context``
277
        An dictionary of variables to add to the template context.
278

279
    **Template:**
280

281
    im/profile.html or ``template_name`` keyword argument.
282

283
    **Settings:**
284

285
    The view expectes the following settings are defined:
286

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

    
318

    
319
@transaction.commit_manually
320
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
321
    """
322
    Allows a user to create a local account.
323

324
    In case of GET request renders a form for entering the user information.
325
    In case of POST handles the signup.
326

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

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

335
    On unsuccessful creation, renders ``template_name`` with an error message.
336

337
    **Arguments**
338

339
    ``template_name``
340
        A custom template to render. This is optional;
341
        if not specified, this will default to ``im/signup.html``.
342

343
    ``on_success``
344
        A custom template to render in case of success. This is optional;
345
        if not specified, this will default to ``im/signup_complete.html``.
346

347
    ``extra_context``
348
        An dictionary of variables to add to the template context.
349

350
    **Template:**
351

352
    im/signup.html or ``template_name`` keyword argument.
353
    im/signup_complete.html or ``on_success`` keyword argument.
354
    """
355
    if request.user.is_authenticated():
356
        return HttpResponseRedirect(reverse('edit_profile'))
357

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

    
403

    
404
@login_required
405
@signed_terms_required
406
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
407
    """
408
    Allows a user to send feedback.
409

410
    In case of GET request renders a form for providing the feedback information.
411
    In case of POST sends an email to support team.
412

413
    If the user isn't logged in, redirects to settings.LOGIN_URL.
414

415
    **Arguments**
416

417
    ``template_name``
418
        A custom template to use. This is optional; if not specified,
419
        this will default to ``im/feedback.html``.
420

421
    ``extra_context``
422
        An dictionary of variables to add to the template context.
423

424
    **Template:**
425

426
    im/signup.html or ``template_name`` keyword argument.
427

428
    **Settings:**
429

430
    * LOGIN_URL: login uri
431
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
432
    """
433
    if request.method == 'GET':
434
        form = FeedbackForm()
435
    if request.method == 'POST':
436
        if not request.user:
437
            return HttpResponse('Unauthorized', status=401)
438

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

    
454

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

    
482

    
483
@transaction.commit_manually
484
def activate(request, greeting_email_template_name='im/welcome_email.txt',
485
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
486
    """
487
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
488
    and renews the user token.
489

490
    The view uses commit_manually decorator in order to ensure the user state will be updated
491
    only if the email will be send successfully.
492
    """
493
    token = request.GET.get('auth')
494
    next = request.GET.get('next')
495
    try:
496
        user = AstakosUser.objects.get(auth_token=token)
497
    except AstakosUser.DoesNotExist:
498
        return HttpResponseBadRequest(_('No such user'))
499

    
500
    if user.is_active:
501
        message = _('Account already active.')
502
        messages.error(request, message)
503
        return index(request)
504

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

    
555

    
556
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
557
    term = None
558
    terms = None
559
    if not term_id:
560
        try:
561
            term = ApprovalTerms.objects.order_by('-id')[0]
562
        except IndexError:
563
            pass
564
    else:
565
        try:
566
            term = ApprovalTerms.objects.get(id=term_id)
567
        except ApprovalTerms.DoesNotExist, e:
568
            pass
569

    
570
    if not term:
571
        messages.error(request, 'There are no approval terms.')
572
        return HttpResponseRedirect(reverse('index'))
573
    f = open(term.location, 'r')
574
    terms = f.read()
575

    
576
    if request.method == 'POST':
577
        next = request.POST.get('next')
578
        if not next:
579
            next = reverse('index')
580
        form = SignApprovalTermsForm(request.POST, instance=request.user)
581
        if not form.is_valid():
582
            return render_response(template_name,
583
                                   terms=terms,
584
                                   approval_terms_form=form,
585
                                   context_instance=get_context(request, extra_context))
586
        user = form.save()
587
        return HttpResponseRedirect(next)
588
    else:
589
        form = None
590
        if request.user.is_authenticated() and not request.user.signed_terms:
591
            form = SignApprovalTermsForm(instance=request.user)
592
        return render_response(template_name,
593
                               terms=terms,
594
                               approval_terms_form=form,
595
                               context_instance=get_context(request, extra_context))
596

    
597

    
598
@signed_terms_required
599
def change_password(request):
600
    return password_change(request,
601
                           post_change_redirect=reverse('edit_profile'),
602
                           password_change_form=ExtendedPasswordChangeForm)
603

    
604

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

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

    
657

    
658
@signed_terms_required
659
@login_required
660
def group_add(request, kind_name='default'):
661
    try:
662
        kind = GroupKind.objects.get(name=kind_name)
663
    except:
664
        return HttpResponseBadRequest(_('No such group kind'))
665

    
666
    post_save_redirect = '/im/group/%(id)s/'
667
    context_processors = None
668
    model, form_class = get_model_and_form_class(
669
        model=None,
670
        form_class=AstakosGroupCreationForm
671
    )
672
    resources = dict(
673
        (str(r.id), r) for r in Resource.objects.select_related().all())
674
    policies = []
675
    if request.method == 'POST':
676
        form = form_class(request.POST, request.FILES, resources=resources)
677
        if form.is_valid():
678
            new_object = form.save()
679

    
680
            # save owner
681
            new_object.owners = [request.user]
682

    
683
            # save quota policies
684
            for (rid, uplimit) in form.resources():
685
                try:
686
                    r = resources[rid]
687
                except KeyError, e:
688
                    logger.exception(e)
689
                    # TODO Should I stay or should I go???
690
                    continue
691
                else:
692
                    new_object.astakosgroupquota_set.create(
693
                        resource=r,
694
                        uplimit=uplimit
695
                    )
696
                policies.append('%s %d' % (r, uplimit))
697
            msg = _("The %(verbose_name)s was created successfully.") %\
698
                {"verbose_name": model._meta.verbose_name}
699
            messages.success(request, msg, fail_silently=True)
700

    
701
            # send notification
702
            try:
703
                send_group_creation_notification(
704
                    template_name='im/group_creation_notification.txt',
705
                    dictionary={
706
                        'group': new_object,
707
                        'owner': request.user,
708
                        'policies': policies,
709
                    }
710
                )
711
            except SendNotificationError, e:
712
                messages.error(request, e, fail_silently=True)
713
            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
714
    else:
715
        now = datetime.now()
716
        data = {
717
            'kind': kind
718
        }
719
        form = form_class(data, resources=resources)
720

    
721
    # Create the template, context, response
722
    template_name = "%s/%s_form.html" % (
723
        model._meta.app_label,
724
        model._meta.object_name.lower()
725
    )
726
    t = template_loader.get_template(template_name)
727
    c = RequestContext(request, {
728
        'form': form,
729
        'kind': kind,
730
    }, context_processors)
731
    return HttpResponse(t.render(c))
732

    
733

    
734
@signed_terms_required
735
@login_required
736
def group_list(request):
737
    none = request.user.astakos_groups.none()
738
    q = AstakosGroup.objects.raw("""
739
        SELECT auth_group.id,
740
        %s AS groupname,
741
        im_groupkind.name AS kindname,
742
        im_astakosgroup.*,
743
        owner.email AS groupowner,
744
        (SELECT COUNT(*) FROM im_membership
745
            WHERE group_id = im_astakosgroup.group_ptr_id
746
            AND date_joined IS NOT NULL) AS approved_members_num,
747
        (SELECT CASE WHEN(
748
                    SELECT date_joined FROM im_membership
749
                    WHERE group_id = im_astakosgroup.group_ptr_id
750
                    AND person_id = %s) IS NULL
751
                    THEN 0 ELSE 1 END) AS membership_status
752
        FROM im_astakosgroup
753
        INNER JOIN im_membership ON (
754
            im_astakosgroup.group_ptr_id = im_membership.group_id)
755
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
756
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
757
        LEFT JOIN im_astakosuser_owner ON (
758
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
759
        LEFT JOIN auth_user as owner ON (
760
            im_astakosuser_owner.astakosuser_id = owner.id)
761
        WHERE im_membership.person_id = %s
762
        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
763
    d = defaultdict(list)
764
    for g in q:
765
        if request.user.email == g.groupowner:
766
            d['own'].append(g)
767
        else:
768
            d['other'].append(g)
769

    
770
    # validate sorting
771
    fields = ('own', 'other')
772
    for f in fields:
773
        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
774
        if v:
775
            form = AstakosGroupSortForm({'sort_by': v})
776
            if not form.is_valid():
777
                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
778
    return object_list(request, queryset=none,
779
                       extra_context={'is_search': False,
780
                                      'mine': d['own'],
781
                                      'other': d['other'],
782
                                      'own_sorting': own_sorting,
783
                                      'other_sorting': other_sorting,
784
                                      'own_page': request.GET.get('own_page', 1),
785
                                      'other_page': request.GET.get('other_page', 1)
786
                                      })
787

    
788

    
789
@signed_terms_required
790
@login_required
791
def group_detail(request, group_id):
792
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
793
    q = q.extra(select={
794
        'is_member': """SELECT CASE WHEN EXISTS(
795
                            SELECT id FROM im_membership
796
                            WHERE group_id = im_astakosgroup.group_ptr_id
797
                            AND person_id = %s)
798
                        THEN 1 ELSE 0 END""" % request.user.id,
799
        'is_owner': """SELECT CASE WHEN EXISTS(
800
                        SELECT id FROM im_astakosuser_owner
801
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
802
                        AND astakosuser_id = %s)
803
                        THEN 1 ELSE 0 END""" % request.user.id,
804
        'kindname': """SELECT name FROM im_groupkind
805
                       WHERE id = im_astakosgroup.kind_id"""})
806

    
807
    model = q.model
808
    context_processors = None
809
    mimetype = None
810
    try:
811
        obj = q.get()
812
    except AstakosGroup.DoesNotExist:
813
        raise Http404("No %s found matching the query" % (
814
            model._meta.verbose_name))
815

    
816
    update_form = AstakosGroupUpdateForm(instance=obj)
817
    addmembers_form = AddGroupMembersForm()
818
    if request.method == 'POST':
819
        update_data = {}
820
        addmembers_data = {}
821
        for k, v in request.POST.iteritems():
822
            if k in update_form.fields:
823
                update_data[k] = v
824
            if k in addmembers_form.fields:
825
                addmembers_data[k] = v
826
        update_data = update_data or None
827
        addmembers_data = addmembers_data or None
828
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
829
        addmembers_form = AddGroupMembersForm(addmembers_data)
830
        if update_form.is_valid():
831
            update_form.save()
832
        if addmembers_form.is_valid():
833
            map(obj.approve_member, addmembers_form.valid_users)
834
            addmembers_form = AddGroupMembersForm()
835

    
836
    template_name = "%s/%s_detail.html" % (
837
        model._meta.app_label, model._meta.object_name.lower())
838
    t = template_loader.get_template(template_name)
839
    c = RequestContext(request, {
840
        'object': obj,
841
    }, context_processors)
842

    
843
    # validate sorting
844
    sorting = request.GET.get('sorting')
845
    if sorting:
846
        form = MembersSortForm({'sort_by': sorting})
847
        if form.is_valid():
848
            sorting = form.cleaned_data.get('sort_by')
849

    
850
    extra_context = {'update_form': update_form,
851
                     'addmembers_form': addmembers_form,
852
                     'page': request.GET.get('page', 1),
853
                     'sorting': sorting}
854
    for key, value in extra_context.items():
855
        if callable(value):
856
            c[key] = value()
857
        else:
858
            c[key] = value
859
    response = HttpResponse(t.render(c), mimetype=mimetype)
860
    populate_xheaders(
861
        request, response, model, getattr(obj, obj._meta.pk.name))
862
    return response
863

    
864

    
865
@signed_terms_required
866
@login_required
867
def group_search(request, extra_context=None, **kwargs):
868
    q = request.GET.get('q')
869
    sorting = request.GET.get('sorting')
870
    if request.method == 'GET':
871
        form = AstakosGroupSearchForm({'q': q} if q else None)
872
    else:
873
        form = AstakosGroupSearchForm(get_query(request))
874
        if form.is_valid():
875
            q = form.cleaned_data['q'].strip()
876
    if q:
877
        queryset = AstakosGroup.objects.select_related()
878
        queryset = queryset.filter(name__contains=q)
879
        queryset = queryset.filter(approval_date__isnull=False)
880
        queryset = queryset.extra(select={
881
                                  'groupname': DB_REPLACE_GROUP_SCHEME,
882
                                  'kindname': "im_groupkind.name",
883
                                  'approved_members_num': """
884
                    SELECT COUNT(*) FROM im_membership
885
                    WHERE group_id = im_astakosgroup.group_ptr_id
886
                    AND date_joined IS NOT NULL""",
887
                                  'membership_approval_date': """
888
                    SELECT date_joined FROM im_membership
889
                    WHERE group_id = im_astakosgroup.group_ptr_id
890
                    AND person_id = %s""" % request.user.id,
891
                                  'is_member': """
892
                    SELECT CASE WHEN EXISTS(
893
                    SELECT date_joined FROM im_membership
894
                    WHERE group_id = im_astakosgroup.group_ptr_id
895
                    AND person_id = %s)
896
                    THEN 1 ELSE 0 END""" % request.user.id,
897
                                  'is_owner': """
898
                    SELECT CASE WHEN EXISTS(
899
                    SELECT id FROM im_astakosuser_owner
900
                    WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
901
                    AND astakosuser_id = %s)
902
                    THEN 1 ELSE 0 END""" % request.user.id})
903
        if sorting:
904
            # TODO check sorting value
905
            queryset = queryset.order_by(sorting)
906
    else:
907
        queryset = AstakosGroup.objects.none()
908
    return object_list(
909
        request,
910
        queryset,
911
        paginate_by=PAGINATE_BY,
912
        page=request.GET.get('page') or 1,
913
        template_name='im/astakosgroup_list.html',
914
        extra_context=dict(form=form,
915
                           is_search=True,
916
                           q=q,
917
                           sorting=sorting))
918

    
919

    
920
@signed_terms_required
921
@login_required
922
def group_all(request, extra_context=None, **kwargs):
923
    q = AstakosGroup.objects.select_related()
924
    q = q.filter(approval_date__isnull=False)
925
    q = q.extra(select={
926
                'groupname': DB_REPLACE_GROUP_SCHEME,
927
                'kindname': "im_groupkind.name",
928
                'approved_members_num': """
929
                    SELECT COUNT(*) FROM im_membership
930
                    WHERE group_id = im_astakosgroup.group_ptr_id
931
                    AND date_joined IS NOT NULL""",
932
                'membership_approval_date': """
933
                    SELECT date_joined FROM im_membership
934
                    WHERE group_id = im_astakosgroup.group_ptr_id
935
                    AND person_id = %s""" % request.user.id,
936
                'is_member': """
937
                    SELECT CASE WHEN EXISTS(
938
                    SELECT date_joined FROM im_membership
939
                    WHERE group_id = im_astakosgroup.group_ptr_id
940
                    AND person_id = %s)
941
                    THEN 1 ELSE 0 END""" % request.user.id})
942
    sorting = request.GET.get('sorting')
943
    if sorting:
944
        # TODO check sorting value
945
        q = q.order_by(sorting)
946
    return object_list(
947
        request,
948
        q,
949
        paginate_by=PAGINATE_BY,
950
        page=request.GET.get('page') or 1,
951
        template_name='im/astakosgroup_list.html',
952
        extra_context=dict(form=AstakosGroupSearchForm(),
953
                           is_search=True,
954
                           sorting=sorting))
955

    
956

    
957
@signed_terms_required
958
@login_required
959
def group_join(request, group_id):
960
    m = Membership(group_id=group_id,
961
                   person=request.user,
962
                   date_requested=datetime.now())
963
    try:
964
        m.save()
965
        post_save_redirect = reverse(
966
            'group_detail',
967
            kwargs=dict(group_id=group_id))
968
        return HttpResponseRedirect(post_save_redirect)
969
    except IntegrityError, e:
970
        logger.exception(e)
971
        msg = _('Failed to join group.')
972
        messages.error(request, msg)
973
        return group_search(request)
974

    
975

    
976
@signed_terms_required
977
@login_required
978
def group_leave(request, group_id):
979
    try:
980
        m = Membership.objects.select_related().get(
981
            group__id=group_id,
982
            person=request.user)
983
    except Membership.DoesNotExist:
984
        return HttpResponseBadRequest(_('Invalid membership.'))
985
    if request.user in m.group.owner.all():
986
        return HttpResponseForbidden(_('Owner can not leave the group.'))
987
    return delete_object(
988
        request,
989
        model=Membership,
990
        object_id=m.id,
991
        template_name='im/astakosgroup_list.html',
992
        post_delete_redirect=reverse(
993
            'group_detail',
994
            kwargs=dict(group_id=group_id)))
995

    
996

    
997
def handle_membership(func):
998
    @wraps(func)
999
    def wrapper(request, group_id, user_id):
1000
        try:
1001
            m = Membership.objects.select_related().get(
1002
                group__id=group_id,
1003
                person__id=user_id)
1004
        except Membership.DoesNotExist:
1005
            return HttpResponseBadRequest(_('Invalid membership.'))
1006
        else:
1007
            if request.user not in m.group.owner.all():
1008
                return HttpResponseForbidden(_('User is not a group owner.'))
1009
            func(request, m)
1010
            return group_detail(request, group_id)
1011
    return wrapper
1012

    
1013

    
1014
@signed_terms_required
1015
@login_required
1016
@handle_membership
1017
def approve_member(request, membership):
1018
    try:
1019
        membership.approve()
1020
        realname = membership.person.realname
1021
        msg = _('%s has been successfully joined the group.' % realname)
1022
        messages.success(request, msg)
1023
    except BaseException, e:
1024
        logger.exception(e)
1025
        realname = membership.person.realname
1026
        msg = _('Something went wrong during %s\'s approval.' % realname)
1027
        messages.error(request, msg)
1028

    
1029

    
1030
@signed_terms_required
1031
@login_required
1032
@handle_membership
1033
def disapprove_member(request, membership):
1034
    try:
1035
        membership.disapprove()
1036
        realname = membership.person.realname
1037
        msg = _('%s has been successfully removed from the group.' % realname)
1038
        messages.success(request, msg)
1039
    except BaseException, e:
1040
        logger.exception(e)
1041
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
1042
        messages.error(request, msg)
1043

    
1044

    
1045
@signed_terms_required
1046
@login_required
1047
def resource_list(request):
1048
#     if request.method == 'POST':
1049
#         form = PickResourceForm(request.POST)
1050
#         if form.is_valid():
1051
#             r = form.cleaned_data.get('resource')
1052
#             if r:
1053
#                 groups = request.user.membership_set.only('group').filter(
1054
#                     date_joined__isnull=False)
1055
#                 groups = [g.group_id for g in groups]
1056
#                 q = AstakosGroupQuota.objects.select_related().filter(
1057
#                     resource=r, group__in=groups)
1058
#     else:
1059
#         form = PickResourceForm()
1060
#         q = AstakosGroupQuota.objects.none()
1061
#
1062
#     return object_list(request, q,
1063
#                        template_name='im/astakosuserquota_list.html',
1064
#                        extra_context={'form': form, 'data':data})
1065

    
1066
    def with_class(entry):
1067
        entry['load_class'] = 'red'
1068
        max_value = float(entry['maxValue'])
1069
        curr_value = float(entry['currValue'])
1070
        entry['ratio'] = (curr_value / max_value) * 100
1071
        if entry['ratio'] < 66:
1072
            entry['load_class'] = 'yellow'
1073
        if entry['ratio'] < 33:
1074
            entry['load_class'] = 'green'
1075

    
1076
        return entry
1077

    
1078
    def pluralize(entry):
1079
        entry['plural'] = engine.plural(entry.get('name'))
1080
        return entry
1081

    
1082
    c = AstakosDjangoDBCallpoint()
1083
    try:
1084
        data = c.get_user_status(request.user.id)
1085
    except Exception, e:
1086
        data = None
1087
        messages.error(request, e)
1088
    else:
1089
        backenddata = map(with_class, data)
1090
        data = map(pluralize, data)
1091
    return render_response('im/resource_list.html',
1092
                           data=data,
1093
                           context_instance=get_context(request))
1094

    
1095
@signed_terms_required
1096
@login_required
1097
def group_create_demo(request, kind_name='default'):
1098
    resources = callpoint.list_resources()
1099
    resource_catalog = {'resources':defaultdict(defaultdict),
1100
                        'groups':defaultdict(list)}
1101
    for r in resources:
1102
        service = r.get('service', '')
1103
        name = r.get('name', '')
1104
        group = r.get('group', '')
1105
        unit = r.get('unit', '')
1106
        fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
1107
        resource_catalog['resources'][fullname] = dict(unit=unit)
1108
        resource_catalog['groups'][group].append(fullname)
1109
    
1110
    resource_catalog = dict(resource_catalog)
1111
    for k, v in resource_catalog.iteritems():
1112
        resource_catalog[k] = dict(v)
1113
    try:
1114
        kind = GroupKind.objects.get(name=kind_name)
1115
    except:
1116
        return HttpResponseBadRequest(_('No such group kind'))
1117

    
1118
    post_save_redirect = '/im/group/%(id)s/'
1119
    context_processors = None
1120
    model, form_class = get_model_and_form_class(
1121
        model=None,
1122
        form_class=AstakosGroupCreationForm
1123
    )
1124
    
1125
    if request.method == 'POST':
1126
        form = form_class(request.POST, request.FILES)
1127
        if form.is_valid():
1128
            new_object = form.save()
1129
            new_object.policies = form.policies()
1130

    
1131
            # save owner
1132
            new_object.owners = [request.user]
1133
            
1134
            msg = _("The %(verbose_name)s was created successfully.") %\
1135
                {"verbose_name": model._meta.verbose_name}
1136
            messages.success(request, msg, fail_silently=True)
1137

    
1138
            # send notification
1139
            try:
1140
                send_group_creation_notification(
1141
                    template_name='im/group_creation_notification.txt',
1142
                    dictionary={
1143
                        'group': new_object,
1144
                        'owner': request.user,
1145
                        'policies': list(form.policies()),
1146
                    }
1147
                )
1148
            except SendNotificationError, e:
1149
                messages.error(request, e, fail_silently=True)
1150
            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
1151
    else:
1152
        now = datetime.now()
1153
        data = {
1154
            'kind': kind
1155
        }
1156
        form = form_class(data)
1157

    
1158
    # Create the template, context, response
1159
    template_name = "%s/%s_form_demo.html" % (
1160
        model._meta.app_label,
1161
        model._meta.object_name.lower()
1162
    )
1163
    t = template_loader.get_template(template_name)
1164
    c = RequestContext(request, {
1165
        'form': form,
1166
        'kind': kind,
1167
        'resource_catalog':resource_catalog
1168
    }, context_processors)
1169
    return HttpResponse(t.render(c))
1170

    
1171

    
1172
def group_create_list(request):
1173
    form = P ickResourceForm()
1174
    return render_response(
1175
        template='im/astakosgroup_create_list.html',
1176
        context_instance=get_context(request),)
1177

    
1178

    
1179
@signed_terms_required
1180
@login_required
1181
def billing(request):
1182

    
1183
    today = datetime.today()
1184
    month_last_day = calendar.monthrange(today.year, today.month)[1]
1185
    data['resources'] = map(with_class, data['resources'])
1186
    start = request.POST.get('datefrom', None)
1187
    if start:
1188
        today = datetime.fromtimestamp(int(start))
1189
        month_last_day = calendar.monthrange(today.year, today.month)[1]
1190

    
1191
    start = datetime(today.year, today.month, 1).strftime("%s")
1192
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1193
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1194
                                    int(start) * 1000,
1195
                                    int(end) * 1000))
1196
    data = {}
1197

    
1198
    try:
1199
        status, data = r.result
1200
        data = _clear_billing_data(data)
1201
        if status != 200:
1202
            messages.error(request, _('Service response status: %d' % status))
1203
    except:
1204
        messages.error(request, r.result)
1205

    
1206
    print type(start)
1207

    
1208
    return render_response(
1209
        template='im/billing.html',
1210
        context_instance=get_context(request),
1211
        data=data,
1212
        zerodate=datetime(month=1, year=1970, day=1),
1213
        today=today,
1214
        start=int(start),
1215
        month_last_day=month_last_day)
1216

    
1217

    
1218
def _clear_billing_data(data):
1219

    
1220
    # remove addcredits entries
1221
    def isnotcredit(e):
1222
        return e['serviceName'] != "addcredits"
1223

    
1224
    # separate services
1225
    def servicefilter(service_name):
1226
        service = service_name
1227

    
1228
        def fltr(e):
1229
            return e['serviceName'] == service
1230
        return fltr
1231

    
1232
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1233
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1234
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1235
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1236

    
1237
    return data
1238

    
1239

    
1240
def group_create_demo(request):
1241
    return render_response(
1242
        template='im/astakosgroup_form_demo.html',
1243
        context_instance=get_context(request))
1244

    
1245
    return data
1246
     
1247
     
1248
@signed_terms_required
1249
@login_required
1250
def timeline(request):
1251
#    data = {'entity':request.user.email}
1252
    timeline_body = ()
1253
    timeline_header = ()
1254
#    form = TimelineForm(data)
1255
    form = TimelineForm()
1256
    if request.method == 'POST':
1257
        data = request.POST
1258
        form = TimelineForm(data)
1259
        if form.is_valid():
1260
            data = form.cleaned_data
1261
            timeline_header = ('entity', 'resource',
1262
                               'event name', 'event date',
1263
                               'incremental cost', 'total cost')
1264
            timeline_body = timeline_charge(
1265
                data['entity'], data['resource'],
1266
                data['start_date'], data['end_date'],
1267
                data['details'], data['operation'])
1268

    
1269
    return render_response(template='im/timeline.html',
1270
                           context_instance=get_context(request),
1271
                           form=form,
1272
                           timeline_header=timeline_header,
1273
                           timeline_body=timeline_body)
1274
    return data