Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 574264f8

History | View | Annotate | Download (41.3 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
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.paginator import Paginator, InvalidPage
61

    
62
from astakos.im.models import (
63
    AstakosUser, ApprovalTerms, AstakosGroup, Resource,
64
    EmailChange, GroupKind, Membership)
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, TimelineForm)
73
from astakos.im.functions import (send_feedback, SendMailError,
74
                                  invite as invite_func, logout as auth_logout,
75
                                  activate as activate_func,
76
                                  switch_account_to_shibboleth,
77
                                  send_admin_notification,
78
                                  SendNotificationError)
79
from astakos.im.endpoints.quotaholder import timeline_charge
80
from astakos.im.settings import (
81
    COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
82
    LOGGING_LEVEL, PAGINATE_BY)
83
from astakos.im.tasks import request_billing
84

    
85
logger = logging.getLogger(__name__)
86

    
87

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

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

    
108

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

    
123

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

    
139

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

145
    **Arguments**
146

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

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

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

158
    **Template:**
159

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

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

    
170

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

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

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

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

187
    **Arguments**
188

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

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

196
    **Template:**
197

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

200
    **Settings:**
201

202
    The view expectes the following settings are defined:
203

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

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

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

    
247

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

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

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

260
    **Arguments**
261

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

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

269
    **Template:**
270

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

273
    **Settings:**
274

275
    The view expectes the following settings are defined:
276

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

    
308

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

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

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

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

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

327
    **Arguments**
328

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

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

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

340
    **Template:**
341

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

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

    
393

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

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

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

405
    **Arguments**
406

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

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

414
    **Template:**
415

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

418
    **Settings:**
419

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

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

    
444

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

    
471

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

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

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

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

    
543

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

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

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

    
584

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

    
591

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

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

    
643

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

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

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

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

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

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

    
721

    
722
@signed_terms_required
723
@login_required
724
def group_list(request):
725
    none = request.user.astakos_groups.none()
726
    q = AstakosGroup.objects.raw("""
727
        SELECT auth_group.id,
728
        %s AS groupname,
729
        im_groupkind.name AS kindname,
730
        im_astakosgroup.*,
731
        owner.email AS groupowner,
732
        (SELECT COUNT(*) FROM im_membership
733
            WHERE group_id = im_astakosgroup.group_ptr_id
734
            AND date_joined IS NOT NULL) AS approved_members_num,
735
        (SELECT date_joined FROM im_membership
736
            WHERE group_id = im_astakosgroup.group_ptr_id
737
            AND person_id = %s) AS membership_approval_date
738
        FROM im_astakosgroup
739
        INNER JOIN im_membership ON (
740
            im_astakosgroup.group_ptr_id = im_membership.group_id)
741
        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
742
        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
743
        LEFT JOIN im_astakosuser_owner ON (
744
            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
745
        LEFT JOIN auth_user as owner ON (
746
            im_astakosuser_owner.astakosuser_id = owner.id)
747
        WHERE im_membership.person_id = %s
748
        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
749
    d = defaultdict(list)
750
    for g in q:
751
        if request.user.email == g.groupowner:
752
            d['own'].append(g)
753
        else:
754
            d['other'].append(g)
755
    d.set_default('own', []) 
756
    d.set_default('other', []) 
757
    for k, l in d.iteritems():
758
        page = request.GET.get('%s_page' % k, 1)
759
        sorting = globals()['%s_sorting' % k] = request.GET.get('%s_sorting' % k)
760
        if sorting:
761
            sort_form = AstakosGroupSortForm({'sort_by': sorting})
762
            if sort_form.is_valid():
763
                l.sort(key=lambda i: getattr(i, sorting))
764
                globals()['%s_sorting' % k] = sorting
765
        paginator = Paginator(l, PAGINATE_BY)
766
        
767
        try:
768
            page_number = int(page)
769
        except ValueError:
770
            if page == 'last':
771
                page_number = paginator.num_pages
772
            else:
773
                # Page is not 'last', nor can it be converted to an int.
774
                raise Http404
775
        try:
776
            page_obj = globals()['%s_page_obj' % k] = paginator.page(page_number)
777
        except InvalidPage:
778
            raise Http404
779
    return object_list(request, queryset=none,
780
                       extra_context={'is_search':False,
781
                                      'mine': own_page_obj,
782
                                      'other': other_page_obj,
783
                                      'own_sorting': own_sorting,
784
                                      'other_sorting': other_sorting
785
                                      })
786

    
787

    
788
@signed_terms_required
789
@login_required
790
def group_detail(request, group_id):
791
    try:
792
        group = AstakosGroup.objects.select_related().get(id=group_id)
793
    except AstakosGroup.DoesNotExist:
794
        return HttpResponseBadRequest(_('Invalid group.'))
795
    form = AstakosGroupUpdateForm(instance=group)
796
    search_form = AddGroupMembersForm()
797
    return object_detail(request,
798
                         AstakosGroup.objects.all(),
799
                         object_id=group_id,
800
                         extra_context={'quota': group.quota,
801
                                        'form': form,
802
                                        'search_form': search_form}
803
                         )
804

    
805

    
806
@signed_terms_required
807
@login_required
808
def group_update(request, group_id):
809
    if request.method != 'POST':
810
        return HttpResponseBadRequest('Method not allowed.')
811
    try:
812
        group = AstakosGroup.objects.select_related().get(id=group_id)
813
    except AstakosGroup.DoesNotExist:
814
        return HttpResponseBadRequest(_('Invalid group.'))
815
    form = AstakosGroupUpdateForm(request.POST, instance=group)
816
    if form.is_valid():
817
        form.save()
818
    search_form = AddGroupMembersForm()
819
    return object_detail(request,
820
                         AstakosGroup.objects.all(),
821
                         object_id=group_id,
822
                         extra_context={'quota': group.quota,
823
                                        'form': form,
824
                                        'search_form': search_form})
825

    
826
@signed_terms_required
827
@login_required
828
def group_search(request, extra_context=None, **kwargs):
829
    q = request.GET.get('q')
830
    if request.method == 'GET':
831
        form = AstakosGroupSearchForm({'q': q} if q else None)
832
    else:
833
        form = AstakosGroupSearchForm(get_query(request))
834
        if form.is_valid():
835
            q = form.cleaned_data['q'].strip()
836
    if q:
837
        queryset = AstakosGroup.objects.select_related()
838
        queryset = queryset.filter(name__contains=q)
839
        queryset = queryset.filter(approval_date__isnull=False)
840
        queryset = queryset.extra(select={
841
                'groupname': DB_REPLACE_GROUP_SCHEME,
842
                'kindname': "im_groupkind.name",
843
                'approved_members_num': """
844
                    SELECT COUNT(*) FROM im_membership
845
                    WHERE group_id = im_astakosgroup.group_ptr_id
846
                    AND date_joined IS NOT NULL""",
847
                'membership_approval_date': """
848
                    SELECT date_joined FROM im_membership
849
                    WHERE group_id = im_astakosgroup.group_ptr_id
850
                    AND person_id = %s""" % request.user.id,
851
                'is_member': """
852
                    SELECT CASE WHEN EXISTS(
853
                    SELECT date_joined FROM im_membership
854
                    WHERE group_id = im_astakosgroup.group_ptr_id
855
                    AND person_id = %s)
856
                    THEN 1 ELSE 0 END""" % request.user.id})
857
    else:
858
        queryset = AstakosGroup.objects.none()
859
    return object_list(
860
        request,
861
        queryset,
862
        paginate_by=PAGINATE_BY,
863
        page=request.GET.get('page') or 1,
864
        template_name='im/astakosgroup_list.html',
865
        extra_context=dict(form=form,
866
                           is_search=True,
867
                           q=q))
868

    
869
@signed_terms_required
870
@login_required
871
def group_all(request, extra_context=None, **kwargs):
872
    q = AstakosGroup.objects.select_related()
873
    q = q.filter(approval_date__isnull=False)
874
    q = q.extra(select={
875
                'groupname': DB_REPLACE_GROUP_SCHEME,
876
                'kindname': "im_groupkind.name",
877
                'approved_members_num': """
878
                    SELECT COUNT(*) FROM im_membership
879
                    WHERE group_id = im_astakosgroup.group_ptr_id
880
                    AND date_joined IS NOT NULL""",
881
                'membership_approval_date': """
882
                    SELECT date_joined FROM im_membership
883
                    WHERE group_id = im_astakosgroup.group_ptr_id
884
                    AND person_id = %s""" % request.user.id,
885
                'is_member': """
886
                    SELECT CASE WHEN EXISTS(
887
                    SELECT date_joined FROM im_membership
888
                    WHERE group_id = im_astakosgroup.group_ptr_id
889
                    AND person_id = %s)
890
                    THEN 1 ELSE 0 END""" % request.user.id})
891
    return object_list(
892
                request,
893
                q,
894
                paginate_by=PAGINATE_BY,
895
                page=request.GET.get('page') or 1,
896
                template_name='im/astakosgroup_list.html',
897
                extra_context=dict(form=AstakosGroupSearchForm(),
898
                                   is_search=True))
899

    
900

    
901
@signed_terms_required
902
@login_required
903
def group_join(request, group_id):
904
    m = Membership(group_id=group_id,
905
                   person=request.user,
906
                   date_requested=datetime.now())
907
    try:
908
        m.save()
909
        post_save_redirect = reverse(
910
            'group_detail',
911
            kwargs=dict(group_id=group_id))
912
        return HttpResponseRedirect(post_save_redirect)
913
    except IntegrityError, e:
914
        logger.exception(e)
915
        msg = _('Failed to join group.')
916
        messages.error(request, msg)
917
        return group_search(request)
918

    
919

    
920
@signed_terms_required
921
@login_required
922
def group_leave(request, group_id):
923
    try:
924
        m = Membership.objects.select_related().get(
925
            group__id=group_id,
926
            person=request.user
927
        )
928
    except Membership.DoesNotExist:
929
        return HttpResponseBadRequest(_('Invalid membership.'))
930
    if request.user in m.group.owner.all():
931
        return HttpResponseForbidden(_('Owner can not leave the group.'))
932
    return delete_object(
933
        request,
934
        model=Membership,
935
        object_id=m.id,
936
        template_name='im/astakosgroup_list.html',
937
        post_delete_redirect=reverse(
938
            'group_detail',
939
            kwargs=dict(group_id=group_id)
940
        )
941
    )
942

    
943

    
944
def handle_membership(func):
945
    @wraps(func)
946
    def wrapper(request, group_id, user_id):
947
        try:
948
            m = Membership.objects.select_related().get(
949
                group__id=group_id,
950
                person__id=user_id
951
            )
952
        except Membership.DoesNotExist:
953
            return HttpResponseBadRequest(_('Invalid membership.'))
954
        else:
955
            if request.user not in m.group.owner.all():
956
                return HttpResponseForbidden(_('User is not a group owner.'))
957
            func(request, m)
958
            return render_response(
959
                template='im/astakosgroup_detail.html',
960
                context_instance=get_context(request),
961
                object=m.group,
962
                quota=m.group.quota
963
            )
964
    return wrapper
965

    
966

    
967
@signed_terms_required
968
@login_required
969
@handle_membership
970
def approve_member(request, membership):
971
    try:
972
        membership.approve()
973
        realname = membership.person.realname
974
        msg = _('%s has been successfully joined the group.' % realname)
975
        messages.success(request, msg)
976
    except BaseException, e:
977
        logger.exception(e)
978
        msg = _('Something went wrong during %s\'s approval.' % realname)
979
        messages.error(request, msg)
980

    
981

    
982
@signed_terms_required
983
@login_required
984
@handle_membership
985
def disapprove_member(request, membership):
986
    try:
987
        membership.disapprove()
988
        realname = membership.person.realname
989
        msg = _('%s has been successfully removed from the group.' % realname)
990
        messages.success(request, msg)
991
    except BaseException, e:
992
        logger.exception(e)
993
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
994
        messages.error(request, msg)
995

    
996

    
997

    
998

    
999
@signed_terms_required
1000
@login_required
1001
def add_members(request, group_id):
1002
    if request.method != 'POST':
1003
        return HttpResponseBadRequest(_('Bad method'))
1004
    try:
1005
        group = AstakosGroup.objects.select_related().get(id=group_id)
1006
    except AstakosGroup.DoesNotExist:
1007
        return HttpResponseBadRequest(_('Invalid group.'))
1008
    search_form = AddGroupMembersForm(request.POST)
1009
    if search_form.is_valid():
1010
        users = search_form.get_valid_users()
1011
        map(group.approve_member, users)
1012
        search_form = AddGroupMembersForm()
1013
    form = AstakosGroupUpdateForm(instance=group)
1014
    return object_detail(request,
1015
                         AstakosGroup.objects.all(),
1016
                         object_id=group_id,
1017
                         extra_context={'quota': group.quota,
1018
                                        'form': form,
1019
                                        'search_form' : search_form})
1020

    
1021

    
1022
@signed_terms_required
1023
@login_required
1024
def resource_list(request):
1025
    return render_response(
1026
        template='im/astakosuserquota_list.html',
1027
        context_instance=get_context(request),
1028
        quota=request.user.quota
1029
    )
1030

    
1031

    
1032
def group_create_list(request):
1033
    return render_response(
1034
        template='im/astakosgroup_create_list.html',
1035
        context_instance=get_context(request),
1036
    )
1037

    
1038

    
1039
@signed_terms_required
1040
@login_required
1041
def billing(request):
1042
    
1043
    today = datetime.today()
1044
    month_last_day= calendar.monthrange(today.year, today.month)[1]
1045
    
1046
    start = request.POST.get('datefrom', None)
1047
    if start:
1048
        today = datetime.fromtimestamp(int(start))
1049
        month_last_day= calendar.monthrange(today.year, today.month)[1]
1050
    
1051
    start = datetime(today.year, today.month, 1).strftime("%s")
1052
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1053
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1054
                                    int(start) * 1000,
1055
                                    int(end) * 1000))
1056
    data = {}
1057
    
1058
    try:
1059
        status, data = r.result
1060
        data=clear_billing_data(data)
1061
        if status != 200:
1062
            messages.error(request, _('Service response status: %d' % status))
1063
    except:
1064
        messages.error(request, r.result)
1065
    
1066
    print type(start)
1067
    
1068
    return render_response(
1069
        template='im/billing.html',
1070
        context_instance=get_context(request),
1071
        data=data,
1072
        zerodate=datetime(month=1,year=1970, day=1),
1073
        today=today,
1074
        start=int(start),
1075
        month_last_day=month_last_day)  
1076
    
1077
def clear_billing_data(data):
1078
    
1079
    # remove addcredits entries
1080
    def isnotcredit(e):
1081
        return e['serviceName'] != "addcredits"
1082
    
1083
    
1084
    
1085
    # separate services    
1086
    def servicefilter(service_name):
1087
        service = service_name
1088
        def fltr(e):
1089
            return e['serviceName'] == service
1090
        return fltr
1091
        
1092
    
1093
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1094
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1095
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1096
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1097
        
1098
    return data    
1099

    
1100
@signed_terms_required
1101
@login_required
1102
def timeline(request):
1103
    data = {'entity':request.user.email}
1104
    timeline_body = ()
1105
    timeline_header = ()
1106
    form = TimelineForm(data)
1107
    if request.method == 'POST':
1108
        data = request.POST
1109
        form = TimelineForm(data)
1110
        if form.is_valid():
1111
            data = form.cleaned_data
1112
            timeline_header = ('entity', 'resource',
1113
                               'event name', 'event date',
1114
                               'incremental cost', 'total cost')
1115
            timeline_body = timeline_charge(
1116
                                    data['entity'],     data['resource'],
1117
                                    data['start_date'], data['end_date'],
1118
                                    data['details'],    data['operation'])
1119
        
1120
    return render_response(template='im/timeline.html',
1121
                           context_instance=get_context(request),
1122
                           form=form,
1123
                           timeline_header=timeline_header,
1124
                           timeline_body=timeline_body)
1125
                           l=l)