Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (40.9 kB)

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

    
34
import logging
35
import calendar
36

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

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

    
62
from astakos.im.models import (
63
    AstakosUser, ApprovalTerms, AstakosGroup, Resource,
64
    EmailChange, GroupKind, Membership)
65
from astakos.im.activation_backends import get_backend, SimpleBackend
66
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
67
from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
68
                              FeedbackForm, SignApprovalTermsForm,
69
                              ExtendedPasswordChangeForm, EmailChangeForm,
70
                              AstakosGroupCreationForm, AstakosGroupSearchForm,
71
                              AstakosGroupUpdateForm, AddGroupMembersForm,
72
                              AstakosGroupSortForm, MembersSortForm)
73
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.settings import (COOKIE_NAME, COOKIE_DOMAIN, SITENAME,
80
                                 LOGOUT_NEXT, LOGGING_LEVEL, PAGINATE_BY)
81
from astakos.im.tasks import request_billing
82

    
83
logger = logging.getLogger(__name__)
84

    
85

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

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

    
106

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

    
121

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

    
137

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

143
    **Arguments**
144

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

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

153
    ``extra_context``
154
        An dictionary of variables to add to the template context.
155

156
    **Template:**
157

158
    im/profile.html or im/login.html or ``template_name`` keyword argument.
159

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

    
168

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

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

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

183
    If the user isn't logged in, redirects to settings.LOGIN_URL.
184

185
    **Arguments**
186

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

191
    ``extra_context``
192
        An dictionary of variables to add to the template context.
193

194
    **Template:**
195

196
    im/invitations.html or ``template_name`` keyword argument.
197

198
    **Settings:**
199

200
    The view expectes the following settings are defined:
201

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

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

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

    
245

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

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

256
    If the user isn't logged in, redirects to settings.LOGIN_URL.
257

258
    **Arguments**
259

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

264
    ``extra_context``
265
        An dictionary of variables to add to the template context.
266

267
    **Template:**
268

269
    im/profile.html or ``template_name`` keyword argument.
270

271
    **Settings:**
272

273
    The view expectes the following settings are defined:
274

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

    
306

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

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

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

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

323
    On unsuccessful creation, renders ``template_name`` with an error message.
324

325
    **Arguments**
326

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

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

335
    ``extra_context``
336
        An dictionary of variables to add to the template context.
337

338
    **Template:**
339

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

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

    
391

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

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

401
    If the user isn't logged in, redirects to settings.LOGIN_URL.
402

403
    **Arguments**
404

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

409
    ``extra_context``
410
        An dictionary of variables to add to the template context.
411

412
    **Template:**
413

414
    im/signup.html or ``template_name`` keyword argument.
415

416
    **Settings:**
417

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

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

    
442

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

    
469

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

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

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

    
491
    try:
492
        local_user = AstakosUser.objects.get(
493
            ~Q(id=user.id),
494
            email=user.email,
495
            is_active=True
496
        )
497
    except AstakosUser.DoesNotExist:
498
        try:
499
            activate_func(
500
                user,
501
                greeting_email_template_name,
502
                helpdesk_email_template_name,
503
                verify_email=True
504
            )
505
            response = prepare_response(request, user, next, renew=True)
506
            transaction.commit()
507
            return response
508
        except SendMailError, e:
509
            message = e.message
510
            messages.error(request, message)
511
            transaction.rollback()
512
            return index(request)
513
        except BaseException, e:
514
            message = _('Something went wrong.')
515
            messages.error(request, message)
516
            logger.exception(e)
517
            transaction.rollback()
518
            return index(request)
519
    else:
520
        try:
521
            user = switch_account_to_shibboleth(
522
                user,
523
                local_user,
524
                greeting_email_template_name
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

    
541

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

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

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

    
582

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

    
589

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

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

    
641

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

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

    
664
            # save owner
665
            new_object.owners = [request.user]
666

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

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

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

    
718

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

    
773

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

    
847

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

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

    
938

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

    
957

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

    
978

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

    
995

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

    
1011

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

    
1026

    
1027
@signed_terms_required
1028
@login_required
1029
def resource_list(request):
1030
    return render_response(
1031
        template='im/astakosuserquota_list.html',
1032
        context_instance=get_context(request))
1033

    
1034

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

    
1040

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

    
1102
def group_create_demo(request):
1103
    return render_response(
1104
        template='im/astakosgroup_form_demo.html',
1105
        context_instance=get_context(request))
1106