Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (47.8 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

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

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

    
71
from astakos.im.activation_backends import get_backend, SimpleBackend
72
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
73
from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
74
                              FeedbackForm, SignApprovalTermsForm,
75
                              ExtendedPasswordChangeForm, EmailChangeForm,
76
                              AstakosGroupCreationForm, AstakosGroupSearchForm,
77
                              AstakosGroupUpdateForm, AddGroupMembersForm,
78
                              AstakosGroupSortForm, MembersSortForm,
79
                              TimelineForm, PickResourceForm)
80
from astakos.im.functions import (send_feedback, SendMailError,
81
                                  logout as auth_logout,
82
                                  activate as activate_func,
83
                                  switch_account_to_shibboleth,
84
                                  send_group_creation_notification,
85
                                  SendNotificationError)
86
from astakos.im.endpoints.quotaholder import timeline_charge
87
from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
88
                                 LOGGING_LEVEL, PAGINATE_BY)
89
from astakos.im.tasks import request_billing
90
from astakos.im.api.callpoint import AstakosDjangoDBCallpoint
91

    
92
logger = logging.getLogger(__name__)
93

    
94

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

    
98
callpoint = AstakosDjangoDBCallpoint()
99

    
100
def render_response(template, tab=None, status=200, reset_cookie=False,
101
                    context_instance=None, **kwargs):
102
    """
103
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
104
    keyword argument and returns an ``django.http.HttpResponse`` with the
105
    specified ``status``.
106
    """
107
    if tab is None:
108
        tab = template.partition('_')[0].partition('.html')[0]
109
    kwargs.setdefault('tab', tab)
110
    html = template_loader.render_to_string(
111
        template, kwargs, context_instance=context_instance)
112
    response = HttpResponse(html, status=status)
113
    if reset_cookie:
114
        set_cookie(response, context_instance['request'].user)
115
    return response
116

    
117

    
118
def requires_anonymous(func):
119
    """
120
    Decorator checkes whether the request.user is not Anonymous and in that case
121
    redirects to `logout`.
122
    """
123
    @wraps(func)
124
    def wrapper(request, *args):
125
        if not request.user.is_anonymous():
126
            next = urlencode({'next': request.build_absolute_uri()})
127
            logout_uri = reverse(logout) + '?' + next
128
            return HttpResponseRedirect(logout_uri)
129
        return func(request, *args)
130
    return wrapper
131

    
132

    
133
def signed_terms_required(func):
134
    """
135
    Decorator checkes whether the request.user is Anonymous and in that case
136
    redirects to `logout`.
137
    """
138
    @wraps(func)
139
    def wrapper(request, *args, **kwargs):
140
        if request.user.is_authenticated() and not request.user.signed_terms:
141
            params = urlencode({'next': request.build_absolute_uri(),
142
                                'show_form': ''})
143
            terms_uri = reverse('latest_terms') + '?' + params
144
            return HttpResponseRedirect(terms_uri)
145
        return func(request, *args, **kwargs)
146
    return wrapper
147

    
148

    
149
@require_http_methods(["GET", "POST"])
150
@signed_terms_required
151
def index(request, login_template_name='im/login.html', extra_context=None):
152
    """
153
    If there is logged on user renders the profile page otherwise renders login page.
154

155
    **Arguments**
156

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

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

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

168
    **Template:**
169

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

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

    
180

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

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

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

196
    If the user isn't logged in, redirects to settings.LOGIN_URL.
197

198
    **Arguments**
199

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

204
    ``extra_context``
205
        An dictionary of variables to add to the template context.
206

207
    **Template:**
208

209
    im/invitations.html or ``template_name`` keyword argument.
210

211
    **Settings:**
212

213
    The view expectes the following settings are defined:
214

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

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

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

    
259

    
260
@require_http_methods(["GET", "POST"])
261
@login_required
262
@signed_terms_required
263
def edit_profile(request, template_name='im/profile.html', extra_context=None):
264
    """
265
    Allows a user to edit his/her profile.
266

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

271
    If the user isn't logged in, redirects to settings.LOGIN_URL.
272

273
    **Arguments**
274

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

279
    ``extra_context``
280
        An dictionary of variables to add to the template context.
281

282
    **Template:**
283

284
    im/profile.html or ``template_name`` keyword argument.
285

286
    **Settings:**
287

288
    The view expectes the following settings are defined:
289

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

    
321

    
322
@transaction.commit_manually
323
@require_http_methods(["GET", "POST"])
324
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
325
    """
326
    Allows a user to create a local account.
327

328
    In case of GET request renders a form for entering the user information.
329
    In case of POST handles the signup.
330

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

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

339
    On unsuccessful creation, renders ``template_name`` with an error message.
340

341
    **Arguments**
342

343
    ``template_name``
344
        A custom template to render. This is optional;
345
        if not specified, this will default to ``im/signup.html``.
346

347
    ``on_success``
348
        A custom template to render in case of success. This is optional;
349
        if not specified, this will default to ``im/signup_complete.html``.
350

351
    ``extra_context``
352
        An dictionary of variables to add to the template context.
353

354
    **Template:**
355

356
    im/signup.html or ``template_name`` keyword argument.
357
    im/signup_complete.html or ``on_success`` keyword argument.
358
    """
359
    if request.user.is_authenticated():
360
        return HttpResponseRedirect(reverse('edit_profile'))
361

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

    
407

    
408
@require_http_methods(["GET", "POST"])
409
@login_required
410
@signed_terms_required
411
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
412
    """
413
    Allows a user to send feedback.
414

415
    In case of GET request renders a form for providing the feedback information.
416
    In case of POST sends an email to support team.
417

418
    If the user isn't logged in, redirects to settings.LOGIN_URL.
419

420
    **Arguments**
421

422
    ``template_name``
423
        A custom template to use. This is optional; if not specified,
424
        this will default to ``im/feedback.html``.
425

426
    ``extra_context``
427
        An dictionary of variables to add to the template context.
428

429
    **Template:**
430

431
    im/signup.html or ``template_name`` keyword argument.
432

433
    **Settings:**
434

435
    * LOGIN_URL: login uri
436
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
437
    """
438
    if request.method == 'GET':
439
        form = FeedbackForm()
440
    if request.method == 'POST':
441
        if not request.user:
442
            return HttpResponse('Unauthorized', status=401)
443

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

    
459

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

    
488

    
489
@require_http_methods(["GET", "POST"])
490
@transaction.commit_manually
491
def activate(request, greeting_email_template_name='im/welcome_email.txt',
492
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
493
    """
494
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
495
    and renews the user token.
496

497
    The view uses commit_manually decorator in order to ensure the user state will be updated
498
    only if the email will be send successfully.
499
    """
500
    token = request.GET.get('auth')
501
    next = request.GET.get('next')
502
    try:
503
        user = AstakosUser.objects.get(auth_token=token)
504
    except AstakosUser.DoesNotExist:
505
        return HttpResponseBadRequest(_('No such user'))
506

    
507
    if user.is_active:
508
        message = _('Account already active.')
509
        messages.error(request, message)
510
        return index(request)
511

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

    
562

    
563
@require_http_methods(["GET", "POST"])
564
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
565
    term = None
566
    terms = None
567
    if not term_id:
568
        try:
569
            term = ApprovalTerms.objects.order_by('-id')[0]
570
        except IndexError:
571
            pass
572
    else:
573
        try:
574
            term = ApprovalTerms.objects.get(id=term_id)
575
        except ApprovalTerms.DoesNotExist, e:
576
            pass
577

    
578
    if not term:
579
        messages.error(request, 'There are no approval terms.')
580
        return HttpResponseRedirect(reverse('index'))
581
    f = open(term.location, 'r')
582
    terms = f.read()
583

    
584
    if request.method == 'POST':
585
        next = request.POST.get('next')
586
        if not next:
587
            next = reverse('index')
588
        form = SignApprovalTermsForm(request.POST, instance=request.user)
589
        if not form.is_valid():
590
            return render_response(template_name,
591
                                   terms=terms,
592
                                   approval_terms_form=form,
593
                                   context_instance=get_context(request, extra_context))
594
        user = form.save()
595
        return HttpResponseRedirect(next)
596
    else:
597
        form = None
598
        if request.user.is_authenticated() and not request.user.signed_terms:
599
            form = SignApprovalTermsForm(instance=request.user)
600
        return render_response(template_name,
601
                               terms=terms,
602
                               approval_terms_form=form,
603
                               context_instance=get_context(request, extra_context))
604

    
605

    
606
@require_http_methods(["GET", "POST"])
607
@signed_terms_required
608
def change_password(request):
609
    return password_change(request,
610
                           post_change_redirect=reverse('edit_profile'),
611
                           password_change_form=ExtendedPasswordChangeForm)
612

    
613

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

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

    
666

    
667
@signed_terms_required
668
@login_required
669
def group_add(request, kind_name='default'):
670
    try:
671
        kind = GroupKind.objects.get(name=kind_name)
672
    except:
673
        return HttpResponseBadRequest(_('No such group kind'))
674

    
675
    post_save_redirect = '/im/group/%(id)s/'
676
    context_processors = None
677
    model, form_class = get_model_and_form_class(
678
        model=None,
679
        form_class=AstakosGroupCreationForm
680
    )
681
    resources = dict(
682
        (str(r.id), r) for r in Resource.objects.select_related().all())
683
    policies = []
684
    if request.method == 'POST':
685
        form = form_class(request.POST, request.FILES, resources=resources)
686
        if form.is_valid():
687
            new_object = form.save()
688

    
689
            # save owner
690
            new_object.owners = [request.user]
691

    
692
            # save quota policies
693
            for (rid, uplimit) in form.resources():
694
                try:
695
                    r = resources[rid]
696
                except KeyError, e:
697
                    logger.exception(e)
698
                    # TODO Should I stay or should I go???
699
                    continue
700
                else:
701
                    new_object.astakosgroupquota_set.create(
702
                        resource=r,
703
                        uplimit=uplimit
704
                    )
705
                policies.append('%s %d' % (r, uplimit))
706
            msg = _("The %(verbose_name)s was created successfully.") %\
707
                {"verbose_name": model._meta.verbose_name}
708
            messages.success(request, msg, fail_silently=True)
709

    
710
            # send notification
711
            try:
712
                send_group_creation_notification(
713
                    template_name='im/group_creation_notification.txt',
714
                    dictionary={
715
                        'group': new_object,
716
                        'owner': request.user,
717
                        'policies': policies,
718
                    }
719
                )
720
            except SendNotificationError, e:
721
                messages.error(request, e, fail_silently=True)
722
            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
723
    else:
724
        now = datetime.now()
725
        data = {
726
            'kind': kind
727
        }
728
        form = form_class(data, resources=resources)
729

    
730
    # Create the template, context, response
731
    template_name = "%s/%s_form.html" % (
732
        model._meta.app_label,
733
        model._meta.object_name.lower()
734
    )
735
    t = template_loader.get_template(template_name)
736
    c = RequestContext(request, {
737
        'form': form,
738
        'kind': kind,
739
    }, context_processors)
740
    return HttpResponse(t.render(c))
741

    
742

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

    
779
    # validate sorting
780
    fields = ('own', 'other')
781
    for f in fields:
782
        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
783
        if v:
784
            form = AstakosGroupSortForm({'sort_by': v})
785
            if not form.is_valid():
786
                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
787
    return object_list(request, queryset=none,
788
                       extra_context={'is_search': False,
789
                                      'mine': d['own'],
790
                                      'other': d['other'],
791
                                      'own_sorting': own_sorting,
792
                                      'other_sorting': other_sorting,
793
                                      'own_page': request.GET.get('own_page', 1),
794
                                      'other_page': request.GET.get('other_page', 1)
795
                                      })
796

    
797

    
798
@signed_terms_required
799
@login_required
800
def group_detail(request, group_id):
801
    q = AstakosGroup.objects.select_related().filter(pk=group_id)
802
    q = q.extra(select={
803
        'is_member': """SELECT CASE WHEN EXISTS(
804
                            SELECT id FROM im_membership
805
                            WHERE group_id = im_astakosgroup.group_ptr_id
806
                            AND person_id = %s)
807
                        THEN 1 ELSE 0 END""" % request.user.id,
808
        'is_owner': """SELECT CASE WHEN EXISTS(
809
                        SELECT id FROM im_astakosuser_owner
810
                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
811
                        AND astakosuser_id = %s)
812
                        THEN 1 ELSE 0 END""" % request.user.id,
813
        'kindname': """SELECT name FROM im_groupkind
814
                       WHERE id = im_astakosgroup.kind_id"""})
815

    
816
    model = q.model
817
    context_processors = None
818
    mimetype = None
819
    try:
820
        obj = q.get()
821
    except AstakosGroup.DoesNotExist:
822
        raise Http404("No %s found matching the query" % (
823
            model._meta.verbose_name))
824

    
825
    update_form = AstakosGroupUpdateForm(instance=obj)
826
    addmembers_form = AddGroupMembersForm()
827
    if request.method == 'POST':
828
        update_data = {}
829
        addmembers_data = {}
830
        for k, v in request.POST.iteritems():
831
            if k in update_form.fields:
832
                update_data[k] = v
833
            if k in addmembers_form.fields:
834
                addmembers_data[k] = v
835
        update_data = update_data or None
836
        addmembers_data = addmembers_data or None
837
        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
838
        addmembers_form = AddGroupMembersForm(addmembers_data)
839
        if update_form.is_valid():
840
            update_form.save()
841
        if addmembers_form.is_valid():
842
            map(obj.approve_member, addmembers_form.valid_users)
843
            addmembers_form = AddGroupMembersForm()
844

    
845
    template_name = "%s/%s_detail.html" % (
846
        model._meta.app_label, model._meta.object_name.lower())
847
    t = template_loader.get_template(template_name)
848
    c = RequestContext(request, {
849
        'object': obj,
850
    }, context_processors)
851

    
852
    # validate sorting
853
    sorting = request.GET.get('sorting')
854
    if sorting:
855
        form = MembersSortForm({'sort_by': sorting})
856
        if form.is_valid():
857
            sorting = form.cleaned_data.get('sort_by')
858

    
859
    extra_context = {'update_form': update_form,
860
                     'addmembers_form': addmembers_form,
861
                     'page': request.GET.get('page', 1),
862
                     'sorting': sorting}
863
    for key, value in extra_context.items():
864
        if callable(value):
865
            c[key] = value()
866
        else:
867
            c[key] = value
868
    response = HttpResponse(t.render(c), mimetype=mimetype)
869
    populate_xheaders(
870
        request, response, model, getattr(obj, obj._meta.pk.name))
871
    return response
872

    
873

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

    
928

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

    
965

    
966
@signed_terms_required
967
@login_required
968
def group_join(request, group_id):
969
    m = Membership(group_id=group_id,
970
                   person=request.user,
971
                   date_requested=datetime.now())
972
    try:
973
        m.save()
974
        post_save_redirect = reverse(
975
            'group_detail',
976
            kwargs=dict(group_id=group_id))
977
        return HttpResponseRedirect(post_save_redirect)
978
    except IntegrityError, e:
979
        logger.exception(e)
980
        msg = _('Failed to join group.')
981
        messages.error(request, msg)
982
        return group_search(request)
983

    
984

    
985
@signed_terms_required
986
@login_required
987
def group_leave(request, group_id):
988
    try:
989
        m = Membership.objects.select_related().get(
990
            group__id=group_id,
991
            person=request.user)
992
    except Membership.DoesNotExist:
993
        return HttpResponseBadRequest(_('Invalid membership.'))
994
    if request.user in m.group.owner.all():
995
        return HttpResponseForbidden(_('Owner can not leave the group.'))
996
    return delete_object(
997
        request,
998
        model=Membership,
999
        object_id=m.id,
1000
        template_name='im/astakosgroup_list.html',
1001
        post_delete_redirect=reverse(
1002
            'group_detail',
1003
            kwargs=dict(group_id=group_id)))
1004

    
1005

    
1006
def handle_membership(func):
1007
    @wraps(func)
1008
    def wrapper(request, group_id, user_id):
1009
        try:
1010
            m = Membership.objects.select_related().get(
1011
                group__id=group_id,
1012
                person__id=user_id)
1013
        except Membership.DoesNotExist:
1014
            return HttpResponseBadRequest(_('Invalid membership.'))
1015
        else:
1016
            if request.user not in m.group.owner.all():
1017
                return HttpResponseForbidden(_('User is not a group owner.'))
1018
            func(request, m)
1019
            return group_detail(request, group_id)
1020
    return wrapper
1021

    
1022

    
1023
@signed_terms_required
1024
@login_required
1025
@handle_membership
1026
def approve_member(request, membership):
1027
    try:
1028
        membership.approve()
1029
        realname = membership.person.realname
1030
        msg = _('%s has been successfully joined the group.' % realname)
1031
        messages.success(request, msg)
1032
    except BaseException, e:
1033
        logger.exception(e)
1034
        realname = membership.person.realname
1035
        msg = _('Something went wrong during %s\'s approval.' % realname)
1036
        messages.error(request, msg)
1037

    
1038

    
1039
@signed_terms_required
1040
@login_required
1041
@handle_membership
1042
def disapprove_member(request, membership):
1043
    try:
1044
        membership.disapprove()
1045
        realname = membership.person.realname
1046
        msg = _('%s has been successfully removed from the group.' % realname)
1047
        messages.success(request, msg)
1048
    except BaseException, e:
1049
        logger.exception(e)
1050
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
1051
        messages.error(request, msg)
1052

    
1053

    
1054
@signed_terms_required
1055
@login_required
1056
def resource_list(request):
1057
#     if request.method == 'POST':
1058
#         form = PickResourceForm(request.POST)
1059
#         if form.is_valid():
1060
#             r = form.cleaned_data.get('resource')
1061
#             if r:
1062
#                 groups = request.user.membership_set.only('group').filter(
1063
#                     date_joined__isnull=False)
1064
#                 groups = [g.group_id for g in groups]
1065
#                 q = AstakosGroupQuota.objects.select_related().filter(
1066
#                     resource=r, group__in=groups)
1067
#     else:
1068
#         form = PickResourceForm()
1069
#         q = AstakosGroupQuota.objects.none()
1070
#
1071
#     return object_list(request, q,
1072
#                        template_name='im/astakosuserquota_list.html',
1073
#                        extra_context={'form': form, 'data':data})
1074

    
1075
    def with_class(entry):
1076
        entry['load_class'] = 'red'
1077
        max_value = float(entry['maxValue'])
1078
        curr_value = float(entry['currValue'])
1079
        entry['ratio'] = (curr_value / max_value) * 100
1080
        if entry['ratio'] < 66:
1081
            entry['load_class'] = 'yellow'
1082
        if entry['ratio'] < 33:
1083
            entry['load_class'] = 'green'
1084
        return entry
1085

    
1086
    def pluralize(entry):
1087
        entry['plural'] = engine.plural(entry.get('name'))
1088
        return entry
1089

    
1090
    try:
1091
        data = callpoint.get_user_status(request.user.id)
1092
    except Exception, e:
1093
        data = None
1094
        messages.error(request, e)
1095
    else:
1096
        backenddata = map(with_class, data)
1097
        data = map(pluralize, data)
1098
    return render_response('im/resource_list.html',
1099
                           data=data,
1100
                           context_instance=get_context(request))
1101

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

    
1125
    post_save_redirect = '/im/group/%(id)s/'
1126
    context_processors = None
1127
    model, form_class = get_model_and_form_class(
1128
        model=None,
1129
        form_class=AstakosGroupCreationForm
1130
    )
1131
    
1132
    if request.method == 'POST':
1133
        form = form_class(request.POST, request.FILES)
1134
        if form.is_valid():
1135
            new_object = form.save()
1136
            new_object.policies = form.policies()
1137

    
1138
            # save owner
1139
            new_object.owners = [request.user]
1140
            
1141
            msg = _("The %(verbose_name)s was created successfully.") %\
1142
                {"verbose_name": model._meta.verbose_name}
1143
            messages.success(request, msg, fail_silently=True)
1144

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

    
1165
    resource_presentation = {
1166
       'compute': {
1167
            'help_text':'group compute help text',
1168
                     
1169
        },
1170
        'storage': {
1171
            'help_text':'group storage help text',
1172
                      
1173
        },  
1174
        'pithos+.diskspace': {
1175
            'help_text':'resource pithos+.diskspace help text',
1176
                      
1177
        },  
1178
        'cyclades.vm': {
1179
            'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
1180
                      
1181
        },  
1182
        'cyclades.disksize': {
1183
            'help_text':'resource cyclades.disksize help text',
1184
                      
1185
        },  
1186
        'cyclades.ram': {
1187
            'help_text':'resource cyclades.ram help text',
1188
                      
1189
        }                        
1190
                             
1191
    }
1192
    
1193
    # Create the template, context, response
1194
    template_name = "%s/%s_form_demo.html" % (
1195
        model._meta.app_label,
1196
        model._meta.object_name.lower()
1197
    )
1198
    t = template_loader.get_template(template_name)
1199
    c = RequestContext(request, {
1200
        'form': form,
1201
        'kind': kind,
1202
        'resource_catalog':resource_catalog,
1203
        'resource_presentation':resource_presentation
1204
    }, context_processors)
1205
    return HttpResponse(t.render(c))
1206

    
1207

    
1208
def group_create_list(request):
1209
    form = PickResourceForm()
1210
    return render_response(
1211
        template='im/astakosgroup_create_list.html',
1212
        context_instance=get_context(request),)
1213

    
1214

    
1215
@signed_terms_required
1216
@login_required
1217
def billing(request):
1218

    
1219
    today = datetime.today()
1220
    month_last_day = calendar.monthrange(today.year, today.month)[1]
1221
    data['resources'] = map(with_class, data['resources'])
1222
    start = request.POST.get('datefrom', None)
1223
    if start:
1224
        today = datetime.fromtimestamp(int(start))
1225
        month_last_day = calendar.monthrange(today.year, today.month)[1]
1226

    
1227
    start = datetime(today.year, today.month, 1).strftime("%s")
1228
    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1229
    r = request_billing.apply(args=('pgerakios@grnet.gr',
1230
                                    int(start) * 1000,
1231
                                    int(end) * 1000))
1232
    data = {}
1233

    
1234
    try:
1235
        status, data = r.result
1236
        data = _clear_billing_data(data)
1237
        if status != 200:
1238
            messages.error(request, _('Service response status: %d' % status))
1239
    except:
1240
        messages.error(request, r.result)
1241

    
1242
    print type(start)
1243

    
1244
    return render_response(
1245
        template='im/billing.html',
1246
        context_instance=get_context(request),
1247
        data=data,
1248
        zerodate=datetime(month=1, year=1970, day=1),
1249
        today=today,
1250
        start=int(start),
1251
        month_last_day=month_last_day)
1252

    
1253

    
1254
def _clear_billing_data(data):
1255

    
1256
    # remove addcredits entries
1257
    def isnotcredit(e):
1258
        return e['serviceName'] != "addcredits"
1259

    
1260
    # separate services
1261
    def servicefilter(service_name):
1262
        service = service_name
1263

    
1264
        def fltr(e):
1265
            return e['serviceName'] == service
1266
        return fltr
1267

    
1268
    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1269
    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1270
    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1271
    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1272

    
1273
    return data
1274
     
1275
     
1276
@signed_terms_required
1277
@login_required
1278
def timeline(request):
1279
#    data = {'entity':request.user.email}
1280
    timeline_body = ()
1281
    timeline_header = ()
1282
#    form = TimelineForm(data)
1283
    form = TimelineForm()
1284
    if request.method == 'POST':
1285
        data = request.POST
1286
        form = TimelineForm(data)
1287
        if form.is_valid():
1288
            data = form.cleaned_data
1289
            timeline_header = ('entity', 'resource',
1290
                               'event name', 'event date',
1291
                               'incremental cost', 'total cost')
1292
            timeline_body = timeline_charge(
1293
                data['entity'], data['resource'],
1294
                data['start_date'], data['end_date'],
1295
                data['details'], data['operation'])
1296

    
1297
    return render_response(template='im/timeline.html',
1298
                           context_instance=get_context(request),
1299
                           form=form,
1300
                           timeline_header=timeline_header,
1301
                           timeline_body=timeline_body)
1302
    return data