Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 9eafaa32

History | View | Annotate | Download (42.6 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

    
37
from urllib import quote
38
from functools import wraps
39
from datetime import datetime, timedelta
40
from collections import defaultdict
41

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

    
62
from astakos.im.models import (
63
    AstakosUser, ApprovalTerms, AstakosGroup, Resource,
64
    EmailChange, GroupKind, Membership, AstakosGroupQuota)
65
from astakos.im.activation_backends import get_backend, SimpleBackend
66
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
67
from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
68
                              FeedbackForm, SignApprovalTermsForm,
69
                              ExtendedPasswordChangeForm, EmailChangeForm,
70
                              AstakosGroupCreationForm, AstakosGroupSearchForm,
71
                              AstakosGroupUpdateForm, AddGroupMembersForm,
72
                              AstakosGroupSortForm, MembersSortForm,
73
                              TimelineForm, PickResourceForm)
74
from astakos.im.functions import (send_feedback, SendMailError,
75
                                  invite as invite_func, logout as auth_logout,
76
                                  activate as activate_func,
77
                                  switch_account_to_shibboleth,
78
                                  send_admin_notification,
79
                                  SendNotificationError)
80
from astakos.im.endpoints.quotaholder import timeline_charge
81
from astakos.im.settings import (
82
    COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
83
    LOGGING_LEVEL, PAGINATE_BY)
84
from astakos.im.tasks import request_billing
85

    
86
logger = logging.getLogger(__name__)
87

    
88

    
89
DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
90
                                     'https://', '')"""
91

    
92
def render_response(template, tab=None, status=200, reset_cookie=False,
93
                    context_instance=None, **kwargs):
94
    """
95
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
96
    keyword argument and returns an ``django.http.HttpResponse`` with the
97
    specified ``status``.
98
    """
99
    if tab is None:
100
        tab = template.partition('_')[0].partition('.html')[0]
101
    kwargs.setdefault('tab', tab)
102
    html = template_loader.render_to_string(
103
        template, kwargs, context_instance=context_instance)
104
    response = HttpResponse(html, status=status)
105
    if reset_cookie:
106
        set_cookie(response, context_instance['request'].user)
107
    return response
108

    
109

    
110
def requires_anonymous(func):
111
    """
112
    Decorator checkes whether the request.user is not Anonymous and in that case
113
    redirects to `logout`.
114
    """
115
    @wraps(func)
116
    def wrapper(request, *args):
117
        if not request.user.is_anonymous():
118
            next = urlencode({'next': request.build_absolute_uri()})
119
            logout_uri = reverse(logout) + '?' + next
120
            return HttpResponseRedirect(logout_uri)
121
        return func(request, *args)
122
    return wrapper
123

    
124

    
125
def signed_terms_required(func):
126
    """
127
    Decorator checkes whether the request.user is Anonymous and in that case
128
    redirects to `logout`.
129
    """
130
    @wraps(func)
131
    def wrapper(request, *args, **kwargs):
132
        if request.user.is_authenticated() and not request.user.signed_terms:
133
            params = urlencode({'next': request.build_absolute_uri(),
134
                                'show_form': ''})
135
            terms_uri = reverse('latest_terms') + '?' + params
136
            return HttpResponseRedirect(terms_uri)
137
        return func(request, *args, **kwargs)
138
    return wrapper
139

    
140

    
141
@signed_terms_required
142
def index(request, login_template_name='im/login.html', extra_context=None):
143
    """
144
    If there is logged on user renders the profile page otherwise renders login page.
145

146
    **Arguments**
147

148
    ``login_template_name``
149
        A custom login template to use. This is optional; if not specified,
150
        this will default to ``im/login.html``.
151

152
    ``profile_template_name``
153
        A custom profile template to use. This is optional; if not specified,
154
        this will default to ``im/profile.html``.
155

156
    ``extra_context``
157
        An dictionary of variables to add to the template context.
158

159
    **Template:**
160

161
    im/profile.html or im/login.html or ``template_name`` keyword argument.
162

163
    """
164
    template_name = login_template_name
165
    if request.user.is_authenticated():
166
        return HttpResponseRedirect(reverse('edit_profile'))
167
    return render_response(template_name,
168
                           login_form=LoginForm(request=request),
169
                           context_instance=get_context(request, extra_context))
170

    
171

    
172
@login_required
173
@signed_terms_required
174
@transaction.commit_manually
175
def invite(request, template_name='im/invitations.html', extra_context=None):
176
    """
177
    Allows a user to invite somebody else.
178

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

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

186
    If the user isn't logged in, redirects to settings.LOGIN_URL.
187

188
    **Arguments**
189

190
    ``template_name``
191
        A custom template to use. This is optional; if not specified,
192
        this will default to ``im/invitations.html``.
193

194
    ``extra_context``
195
        An dictionary of variables to add to the template context.
196

197
    **Template:**
198

199
    im/invitations.html or ``template_name`` keyword argument.
200

201
    **Settings:**
202

203
    The view expectes the following settings are defined:
204

205
    * LOGIN_URL: login uri
206
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
207
    """
208
    status = None
209
    message = None
210
    form = InvitationForm()
211

    
212
    inviter = request.user
213
    if request.method == 'POST':
214
        form = InvitationForm(request.POST)
215
        if inviter.invitations > 0:
216
            if form.is_valid():
217
                try:
218
                    invitation = form.save()
219
                    invite_func(invitation, inviter)
220
                    message = _('Invitation sent to %s' % invitation.username)
221
                    messages.success(request, message)
222
                except SendMailError, e:
223
                    message = e.message
224
                    messages.error(request, message)
225
                    transaction.rollback()
226
                except BaseException, e:
227
                    message = _('Something went wrong.')
228
                    messages.error(request, message)
229
                    logger.exception(e)
230
                    transaction.rollback()
231
                else:
232
                    transaction.commit()
233
        else:
234
            message = _('No invitations left')
235
            messages.error(request, message)
236

    
237
    sent = [{'email': inv.username,
238
             'realname': inv.realname,
239
             'is_consumed': inv.is_consumed}
240
            for inv in request.user.invitations_sent.all()]
241
    kwargs = {'inviter': inviter,
242
              'sent': sent}
243
    context = get_context(request, extra_context, **kwargs)
244
    return render_response(template_name,
245
                           invitation_form=form,
246
                           context_instance=context)
247

    
248

    
249
@login_required
250
@signed_terms_required
251
def edit_profile(request, template_name='im/profile.html', extra_context=None):
252
    """
253
    Allows a user to edit his/her profile.
254

255
    In case of GET request renders a form for displaying the user information.
256
    In case of POST updates the user informantion and redirects to ``next``
257
    url parameter if exists.
258

259
    If the user isn't logged in, redirects to settings.LOGIN_URL.
260

261
    **Arguments**
262

263
    ``template_name``
264
        A custom template to use. This is optional; if not specified,
265
        this will default to ``im/profile.html``.
266

267
    ``extra_context``
268
        An dictionary of variables to add to the template context.
269

270
    **Template:**
271

272
    im/profile.html or ``template_name`` keyword argument.
273

274
    **Settings:**
275

276
    The view expectes the following settings are defined:
277

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

    
309

    
310
@transaction.commit_manually
311
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
312
    """
313
    Allows a user to create a local account.
314

315
    In case of GET request renders a form for entering the user information.
316
    In case of POST handles the signup.
317

318
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
319
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
320
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
321
    (see activation_backends);
322

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

326
    On unsuccessful creation, renders ``template_name`` with an error message.
327

328
    **Arguments**
329

330
    ``template_name``
331
        A custom template to render. This is optional;
332
        if not specified, this will default to ``im/signup.html``.
333

334
    ``on_success``
335
        A custom template to render in case of success. This is optional;
336
        if not specified, this will default to ``im/signup_complete.html``.
337

338
    ``extra_context``
339
        An dictionary of variables to add to the template context.
340

341
    **Template:**
342

343
    im/signup.html or ``template_name`` keyword argument.
344
    im/signup_complete.html or ``on_success`` keyword argument.
345
    """
346
    if request.user.is_authenticated():
347
        return HttpResponseRedirect(reverse('edit_profile'))
348

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

    
394

    
395
@login_required
396
@signed_terms_required
397
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
398
    """
399
    Allows a user to send feedback.
400

401
    In case of GET request renders a form for providing the feedback information.
402
    In case of POST sends an email to support team.
403

404
    If the user isn't logged in, redirects to settings.LOGIN_URL.
405

406
    **Arguments**
407

408
    ``template_name``
409
        A custom template to use. This is optional; if not specified,
410
        this will default to ``im/feedback.html``.
411

412
    ``extra_context``
413
        An dictionary of variables to add to the template context.
414

415
    **Template:**
416

417
    im/signup.html or ``template_name`` keyword argument.
418

419
    **Settings:**
420

421
    * LOGIN_URL: login uri
422
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
423
    """
424
    if request.method == 'GET':
425
        form = FeedbackForm()
426
    if request.method == 'POST':
427
        if not request.user:
428
            return HttpResponse('Unauthorized', status=401)
429

    
430
        form = FeedbackForm(request.POST)
431
        if form.is_valid():
432
            msg = form.cleaned_data['feedback_msg']
433
            data = form.cleaned_data['feedback_data']
434
            try:
435
                send_feedback(msg, data, request.user, email_template_name)
436
            except SendMailError, e:
437
                messages.error(request, message)
438
            else:
439
                message = _('Feedback successfully sent')
440
                messages.success(request, message)
441
    return render_response(template_name,
442
                           feedback_form=form,
443
                           context_instance=get_context(request, extra_context))
444

    
445

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

    
472

    
473
@transaction.commit_manually
474
def activate(request, greeting_email_template_name='im/welcome_email.txt',
475
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
476
    """
477
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
478
    and renews the user token.
479

480
    The view uses commit_manually decorator in order to ensure the user state will be updated
481
    only if the email will be send successfully.
482
    """
483
    token = request.GET.get('auth')
484
    next = request.GET.get('next')
485
    try:
486
        user = AstakosUser.objects.get(auth_token=token)
487
    except AstakosUser.DoesNotExist:
488
        return HttpResponseBadRequest(_('No such user'))
489

    
490
    if user.is_active:
491
        message = _('Account already active.')
492
        messages.error(request, message)
493
        return index(request)
494

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

    
545

    
546
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
547
    term = None
548
    terms = None
549
    if not term_id:
550
        try:
551
            term = ApprovalTerms.objects.order_by('-id')[0]
552
        except IndexError:
553
            pass
554
    else:
555
        try:
556
            term = ApprovalTerms.objects.get(id=term_id)
557
        except ApprovalTerms.DoesNotExist, e:
558
            pass
559

    
560
    if not term:
561
        return HttpResponseRedirect(reverse('index'))
562
    f = open(term.location, 'r')
563
    terms = f.read()
564

    
565
    if request.method == 'POST':
566
        next = request.POST.get('next')
567
        if not next:
568
            next = reverse('index')
569
        form = SignApprovalTermsForm(request.POST, instance=request.user)
570
        if not form.is_valid():
571
            return render_response(template_name,
572
                                   terms=terms,
573
                                   approval_terms_form=form,
574
                                   context_instance=get_context(request, extra_context))
575
        user = form.save()
576
        return HttpResponseRedirect(next)
577
    else:
578
        form = None
579
        if request.user.is_authenticated() and not request.user.signed_terms:
580
            form = SignApprovalTermsForm(instance=request.user)
581
        return render_response(template_name,
582
                               terms=terms,
583
                               approval_terms_form=form,
584
                               context_instance=get_context(request, extra_context))
585

    
586

    
587
@signed_terms_required
588
def change_password(request):
589
    return password_change(request,
590
                           post_change_redirect=reverse('edit_profile'),
591
                           password_change_form=ExtendedPasswordChangeForm)
592

    
593

    
594
@signed_terms_required
595
@login_required
596
@transaction.commit_manually
597
def change_email(request, activation_key=None,
598
                 email_template_name='registration/email_change_email.txt',
599
                 form_template_name='registration/email_change_form.html',
600
                 confirm_template_name='registration/email_change_done.html',
601
                 extra_context=None):
602
    if activation_key:
603
        try:
604
            user = EmailChange.objects.change_email(activation_key)
605
            if request.user.is_authenticated() and request.user == user:
606
                msg = _('Email changed successfully.')
607
                messages.success(request, msg)
608
                auth_logout(request)
609
                response = prepare_response(request, user)
610
                transaction.commit()
611
                return response
612
        except ValueError, e:
613
            messages.error(request, e)
614
        return render_response(confirm_template_name,
615
                               modified_user=user if 'user' in locals(
616
                               ) else None,
617
                               context_instance=get_context(request,
618
                                                            extra_context))
619

    
620
    if not request.user.is_authenticated():
621
        path = quote(request.get_full_path())
622
        url = request.build_absolute_uri(reverse('index'))
623
        return HttpResponseRedirect(url + '?next=' + path)
624
    form = EmailChangeForm(request.POST or None)
625
    if request.method == 'POST' and form.is_valid():
626
        try:
627
            ec = form.save(email_template_name, request)
628
        except SendMailError, e:
629
            msg = e
630
            messages.error(request, msg)
631
            transaction.rollback()
632
        except IntegrityError, e:
633
            msg = _('There is already a pending change email request.')
634
            messages.error(request, msg)
635
        else:
636
            msg = _('Change email request has been registered succefully.\
637
                    You are going to receive a verification email in the new address.')
638
            messages.success(request, msg)
639
            transaction.commit()
640
    return render_response(form_template_name,
641
                           form=form,
642
                           context_instance=get_context(request,
643
                                                        extra_context))
644

    
645

    
646
@signed_terms_required
647
@login_required
648
def group_add(request, kind_name='default'):
649
    try:
650
        kind = GroupKind.objects.get(name=kind_name)
651
    except:
652
        return HttpResponseBadRequest(_('No such group kind'))
653

    
654
    post_save_redirect = '/im/group/%(id)s/'
655
    context_processors = None
656
    model, form_class = get_model_and_form_class(
657
        model=None,
658
        form_class=AstakosGroupCreationForm
659
    )
660
    resources = dict(
661
        (str(r.id), r) for r in Resource.objects.select_related().all())
662
    policies = []
663
    if request.method == 'POST':
664
        form = form_class(request.POST, request.FILES, resources=resources)
665
        if form.is_valid():
666
            new_object = form.save()
667

    
668
            # save owner
669
            new_object.owners = [request.user]
670

    
671
            # save quota policies
672
            for (rid, uplimit) in form.resources():
673
                try:
674
                    r = resources[rid]
675
                except KeyError, e:
676
                    logger.exception(e)
677
                    # TODO Should I stay or should I go???
678
                    continue
679
                else:
680
                    new_object.astakosgroupquota_set.create(
681
                        resource=r,
682
                        uplimit=uplimit
683
                    )
684
                policies.append('%s %d' % (r, uplimit))
685
            msg = _("The %(verbose_name)s was created successfully.") %\
686
                {"verbose_name": model._meta.verbose_name}
687
            messages.success(request, msg, fail_silently=True)
688

    
689
            # send notification
690
            try:
691
                send_admin_notification(
692
                    template_name='im/group_creation_notification.txt',
693
                    dictionary={
694
                        'group': new_object,
695
                        'owner': request.user,
696
                        'policies': policies,
697
                    },
698
                    subject='%s alpha2 testing group creation notification' % SITENAME
699
                )
700
            except SendNotificationError, e:
701
                messages.error(request, e, fail_silently=True)
702
            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
703
    else:
704
        now = datetime.now()
705
        data = {
706
            'kind': kind
707
        }
708
        form = form_class(data, resources=resources)
709

    
710
    # Create the template, context, response
711
    template_name = "%s/%s_form.html" % (
712
        model._meta.app_label,
713
        model._meta.object_name.lower()
714
    )
715
    t = template_loader.get_template(template_name)
716
    c = RequestContext(request, {
717
        'form': form,
718
        'kind': kind,
719
    }, context_processors)
720
    return HttpResponse(t.render(c))
721

    
722

    
723
@signed_terms_required
724
@login_required
725
def group_list(request):
726
    none = request.user.astakos_groups.none()
727
    q = AstakosGroup.objects.raw("""
728
        SELECT auth_group.id,
729
        %s AS groupname,
730
        im_groupkind.name AS kindname,
731
        im_astakosgroup.*,
732
        owner.email AS groupowner,
733
        (SELECT COUNT(*) FROM im_membership
734
            WHERE group_id = im_astakosgroup.group_ptr_id
735
            AND date_joined IS NOT NULL) AS approved_members_num,
736
        (SELECT CASE WHEN(
737
                    SELECT date_joined FROM im_membership
738
                    WHERE group_id = im_astakosgroup.group_ptr_id
739
                    AND person_id = %s) IS NULL
740
                    THEN 0 ELSE 1 END) AS membership_status
741
        FROM im_astakosgroup
742
        INNER JOIN im_membership ON (
743
            im_astakosgroup.group_ptr_id = im_membership.group_id)
744
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
745
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
746
        LEFT JOIN im_astakosuser_owner ON (
747
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
748
        LEFT JOIN auth_user as owner ON (
749
            im_astakosuser_owner.astakosuser_id = owner.id)
750
        WHERE im_membership.person_id = %s
751
        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
752
    d = defaultdict(list)
753
    for g in q:
754
        if request.user.email == g.groupowner:
755
            d['own'].append(g)
756
        else:
757
            d['other'].append(g)
758
    
759
    # validate sorting
760
    fields = ('own', 'other')
761
    for f in fields:
762
        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
763
        if v:
764
            form = AstakosGroupSortForm({'sort_by': v})
765
            if not form.is_valid():
766
                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
767
    return object_list(request, queryset=none,
768
                       extra_context={'is_search':False,
769
                                      'mine': d['own'],
770
                                      'other': d['other'],
771
                                      'own_sorting': own_sorting,
772
                                      'other_sorting': other_sorting,
773
                                      'own_page': request.GET.get('own_page', 1),
774
                                      'other_page': request.GET.get('other_page', 1)
775
                                      })
776

    
777

    
778
@signed_terms_required
779
@login_required
780
def group_detail(request, group_id):
781
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
782
    q = q.extra(select={
783
        'is_member': """SELECT CASE WHEN EXISTS(
784
                            SELECT id FROM im_membership
785
                            WHERE group_id = im_astakosgroup.group_ptr_id
786
                            AND person_id = %s)
787
                        THEN 1 ELSE 0 END""" % request.user.id,
788
        'is_owner': """SELECT CASE WHEN EXISTS(
789
                        SELECT id FROM im_astakosuser_owner
790
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
791
                        AND astakosuser_id = %s)
792
                        THEN 1 ELSE 0 END""" % request.user.id,
793
        'kindname': """SELECT name FROM im_groupkind
794
                       WHERE id = im_astakosgroup.kind_id"""})
795
    
796
    model = q.model
797
    context_processors = None
798
    mimetype = None
799
    try:
800
        obj = q.get()
801
    except AstakosGroup.DoesNotExist:
802
        raise Http404("No %s found matching the query" % (
803
            model._meta.verbose_name))
804
    
805
    update_form = AstakosGroupUpdateForm(instance=obj)
806
    addmembers_form = AddGroupMembersForm()
807
    if request.method == 'POST':
808
        update_data = {}
809
        addmembers_data = {}
810
        for k,v in request.POST.iteritems():
811
            if k in update_form.fields:
812
                update_data[k] = v
813
            if k in addmembers_form.fields:
814
                addmembers_data[k] = v
815
        update_data = update_data or None
816
        addmembers_data = addmembers_data or None
817
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
818
        addmembers_form = AddGroupMembersForm(addmembers_data)
819
        if update_form.is_valid():
820
            update_form.save()
821
        if addmembers_form.is_valid():
822
            map(obj.approve_member, addmembers_form.valid_users)
823
            addmembers_form = AddGroupMembersForm()
824
    
825
    template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
826
    t = template_loader.get_template(template_name)
827
    c = RequestContext(request, {
828
        'object': obj,
829
    }, context_processors)
830
    
831
    # validate sorting
832
    sorting= request.GET.get('sorting')
833
    if sorting:
834
        form = MembersSortForm({'sort_by': sorting})
835
        if form.is_valid():
836
            sorting = form.cleaned_data.get('sort_by')
837
         
838
    extra_context = {'update_form': update_form,
839
                     'addmembers_form': addmembers_form,
840
                     'page': request.GET.get('page', 1),
841
                     'sorting': sorting}
842
    for key, value in extra_context.items():
843
        if callable(value):
844
            c[key] = value()
845
        else:
846
            c[key] = value
847
    response = HttpResponse(t.render(c), mimetype=mimetype)
848
    populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
849
    return response
850

    
851

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

    
906
@signed_terms_required
907
@login_required
908
def group_all(request, extra_context=None, **kwargs):
909
    q = AstakosGroup.objects.select_related()
910
    q = q.filter(approval_date__isnull=False)
911
    q = q.extra(select={
912
                'groupname': DB_REPLACE_GROUP_SCHEME,
913
                'kindname': "im_groupkind.name",
914
                'approved_members_num': """
915
                    SELECT COUNT(*) FROM im_membership
916
                    WHERE group_id = im_astakosgroup.group_ptr_id
917
                    AND date_joined IS NOT NULL""",
918
                'membership_approval_date': """
919
                    SELECT date_joined FROM im_membership
920
                    WHERE group_id = im_astakosgroup.group_ptr_id
921
                    AND person_id = %s""" % request.user.id,
922
                'is_member': """
923
                    SELECT CASE WHEN EXISTS(
924
                    SELECT date_joined FROM im_membership
925
                    WHERE group_id = im_astakosgroup.group_ptr_id
926
                    AND person_id = %s)
927
                    THEN 1 ELSE 0 END""" % request.user.id})
928
    sorting = request.GET.get('sorting')
929
    if sorting:
930
        # TODO check sorting value
931
        q = q.order_by(sorting)
932
    return object_list(
933
                request,
934
                q,
935
                paginate_by=PAGINATE_BY,
936
                page=request.GET.get('page') or 1,
937
                template_name='im/astakosgroup_list.html',
938
                extra_context=dict(form=AstakosGroupSearchForm(),
939
                                   is_search=True,
940
                                   sorting=sorting))
941

    
942

    
943
@signed_terms_required
944
@login_required
945
def group_join(request, group_id):
946
    m = Membership(group_id=group_id,
947
                   person=request.user,
948
                   date_requested=datetime.now())
949
    try:
950
        m.save()
951
        post_save_redirect = reverse(
952
            'group_detail',
953
            kwargs=dict(group_id=group_id))
954
        return HttpResponseRedirect(post_save_redirect)
955
    except IntegrityError, e:
956
        logger.exception(e)
957
        msg = _('Failed to join group.')
958
        messages.error(request, msg)
959
        return group_search(request)
960

    
961

    
962
@signed_terms_required
963
@login_required
964
def group_leave(request, group_id):
965
    try:
966
        m = Membership.objects.select_related().get(
967
            group__id=group_id,
968
            person=request.user)
969
    except Membership.DoesNotExist:
970
        return HttpResponseBadRequest(_('Invalid membership.'))
971
    if request.user in m.group.owner.all():
972
        return HttpResponseForbidden(_('Owner can not leave the group.'))
973
    return delete_object(
974
        request,
975
        model=Membership,
976
        object_id=m.id,
977
        template_name='im/astakosgroup_list.html',
978
        post_delete_redirect=reverse(
979
            'group_detail',
980
            kwargs=dict(group_id=group_id)))
981

    
982

    
983
def handle_membership(func):
984
    @wraps(func)
985
    def wrapper(request, group_id, user_id):
986
        try:
987
            m = Membership.objects.select_related().get(
988
                group__id=group_id,
989
                person__id=user_id)
990
        except Membership.DoesNotExist:
991
            return HttpResponseBadRequest(_('Invalid membership.'))
992
        else:
993
            if request.user not in m.group.owner.all():
994
                return HttpResponseForbidden(_('User is not a group owner.'))
995
            func(request, m)
996
            return group_detail(request, group_id)
997
    return wrapper
998

    
999

    
1000
@signed_terms_required
1001
@login_required
1002
@handle_membership
1003
def approve_member(request, membership):
1004
    try:
1005
        membership.approve()
1006
        realname = membership.person.realname
1007
        msg = _('%s has been successfully joined the group.' % realname)
1008
        messages.success(request, msg)
1009
    except BaseException, e:
1010
        logger.exception(e)
1011
        realname = membership.person.realname
1012
        msg = _('Something went wrong during %s\'s approval.' % realname)
1013
        messages.error(request, msg)
1014

    
1015

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

    
1030

    
1031
@signed_terms_required
1032
@login_required
1033
def resource_list(request):
1034
    if request.method == 'POST':
1035
        form = PickResourceForm(request.POST)
1036
        if form.is_valid():
1037
            r = form.cleaned_data.get('resource')
1038
            if r:
1039
                groups = request.user.membership_set.only('group').filter(
1040
                    date_joined__isnull=False)
1041
                groups = [g.group_id for g in groups]
1042
                q = AstakosGroupQuota.objects.select_related().filter(
1043
                    resource=r, group__in=groups)
1044
    else:
1045
        form = PickResourceForm()
1046
        q = AstakosGroupQuota.objects.none()
1047
    return object_list(request, q,
1048
                       template_name='im/astakosuserquota_list.html',
1049
                       extra_context={'form': form})
1050

    
1051

    
1052
def group_create_list(request):
1053
    form = PickResourceForm()
1054
    return render_response(
1055
        template='im/astakosgroup_create_list.html',
1056
        context_instance=get_context(request),)
1057

    
1058

    
1059
@signed_terms_required
1060
@login_required
1061
def billing(request):
1062
    
1063
    today = datetime.today()
1064
    month_last_day= calendar.monthrange(today.year, today.month)[1]
1065
    
1066
    start = request.POST.get('datefrom', None)
1067
    if start:
1068
        today = datetime.fromtimestamp(int(start))
1069
        month_last_day= calendar.monthrange(today.year, today.month)[1]
1070
    
1071
    start = datetime(today.year, today.month, 1).strftime("%s")
1072
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1073
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1074
                                    int(start) * 1000,
1075
                                    int(end) * 1000))
1076
    data = {}
1077
    
1078
    try:
1079
        status, data = r.result
1080
        data=_clear_billing_data(data)
1081
        if status != 200:
1082
            messages.error(request, _('Service response status: %d' % status))
1083
    except:
1084
        messages.error(request, r.result)
1085
    
1086
    print type(start)
1087
    
1088
    return render_response(
1089
        template='im/billing.html',
1090
        context_instance=get_context(request),
1091
        data=data,
1092
        zerodate=datetime(month=1,year=1970, day=1),
1093
        today=today,
1094
        start=int(start),
1095
        month_last_day=month_last_day)  
1096
    
1097
def _clear_billing_data(data):
1098
    
1099
    # remove addcredits entries
1100
    def isnotcredit(e):
1101
        return e['serviceName'] != "addcredits"
1102
    
1103
    
1104
    
1105
    # separate services    
1106
    def servicefilter(service_name):
1107
        service = service_name
1108
        def fltr(e):
1109
            return e['serviceName'] == service
1110
        return fltr
1111
        
1112
    
1113
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1114
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1115
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1116
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1117
        
1118
    return data    
1119

    
1120
@signed_terms_required
1121
@login_required
1122
def timeline(request):
1123
#    data = {'entity':request.user.email}
1124
    timeline_body = ()
1125
    timeline_header = ()
1126
#    form = TimelineForm(data)
1127
    form = TimelineForm()
1128
    if request.method == 'POST':
1129
        data = request.POST
1130
        form = TimelineForm(data)
1131
        if form.is_valid():
1132
            data = form.cleaned_data
1133
            timeline_header = ('entity', 'resource',
1134
                               'event name', 'event date',
1135
                               'incremental cost', 'total cost')
1136
            timeline_body = timeline_charge(
1137
                                    data['entity'],     data['resource'],
1138
                                    data['start_date'], data['end_date'],
1139
                                    data['details'],    data['operation'])
1140
        
1141
    return render_response(template='im/timeline.html',
1142
                           context_instance=get_context(request),
1143
                           form=form,
1144
                           timeline_header=timeline_header,
1145
                           timeline_body=timeline_body)
1146
    return data