Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 04febd09

History | View | Annotate | Download (41.5 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.db.models.fields import DateTimeField
51
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
52
    HttpResponseRedirect, HttpResponseBadRequest, Http404
53
from django.shortcuts import redirect
54
from django.template import RequestContext, loader
55
from django.utils.http import urlencode
56
from django.utils.translation import ugettext as _
57
from django.views.generic.create_update import (create_object, delete_object,
58
                                                get_model_and_form_class)
59
from django.views.generic.list_detail import object_list, object_detail
60
from django.http import HttpResponseBadRequest
61
from django.core.paginator import Paginator, InvalidPage
62

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

    
85
logger = logging.getLogger(__name__)
86

    
87

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

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

    
108

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

    
123

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

    
139

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

145
    **Arguments**
146

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

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

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

158
    **Template:**
159

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

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

    
170

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

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

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

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

187
    **Arguments**
188

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

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

196
    **Template:**
197

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

200
    **Settings:**
201

202
    The view expectes the following settings are defined:
203

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

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

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

    
247

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

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

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

260
    **Arguments**
261

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

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

269
    **Template:**
270

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

273
    **Settings:**
274

275
    The view expectes the following settings are defined:
276

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

    
308

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

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

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

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

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

327
    **Arguments**
328

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

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

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

340
    **Template:**
341

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

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

    
393

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

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

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

405
    **Arguments**
406

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

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

414
    **Template:**
415

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

418
    **Settings:**
419

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

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

    
444

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

    
471

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

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

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

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

    
543

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

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

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

    
584

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

    
591

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

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

    
643

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

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

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

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

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

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

    
721

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

    
790

    
791
@signed_terms_required
792
@login_required
793
def group_detail(request, group_id):
794
    try:
795
        group = AstakosGroup.objects.select_related().get(id=group_id)
796
    except AstakosGroup.DoesNotExist:
797
        return HttpResponseBadRequest(_('Invalid group.'))
798
    form = AstakosGroupUpdateForm(instance=group)
799
    add_members_form = AddGroupMembersForm()
800
    
801
    # build members
802
    page = request.GET.get('page', 1)
803
    sorting = request.GET.get('sorting')
804
    if sorting:
805
        group.members.sort(key=lambda i: getattr(i, sorting))
806
    paginator = Paginator(group.members, PAGINATE_BY)
807
    
808
    try:
809
        page_number = int(page)
810
    except ValueError:
811
        if page == 'last':
812
            page_number = paginator.num_pages
813
        else:
814
            # Page is not 'last', nor can it be converted to an int.
815
            raise Http404
816
    try:
817
        members_page = globals()['page'] = paginator.page(page_number)
818
    except InvalidPage:
819
        raise Http404
820
    return object_detail(request,
821
                         AstakosGroup.objects.all(),
822
                         object_id=group_id,
823
                         extra_context={'quota': group.quota,
824
                                        'form': form,
825
                                        'search_form': add_members_form,
826
                                        'members': members_page,
827
                                        'sorting': sorting}
828
                         )
829

    
830

    
831
@signed_terms_required
832
@login_required
833
def group_update(request, group_id):
834
    if request.method != 'POST':
835
        return HttpResponseBadRequest('Method not allowed.')
836
    try:
837
        group = AstakosGroup.objects.select_related().get(id=group_id)
838
    except AstakosGroup.DoesNotExist:
839
        return HttpResponseBadRequest(_('Invalid group.'))
840
    form = AstakosGroupUpdateForm(request.POST, instance=group)
841
    if form.is_valid():
842
        form.save()
843
    search_form = AddGroupMembersForm()
844
    return object_detail(request,
845
                         AstakosGroup.objects.all(),
846
                         object_id=group_id,
847
                         extra_context={'quota': group.quota,
848
                                        'form': form,
849
                                        'search_form': search_form})
850

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

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

    
935

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

    
954

    
955
@signed_terms_required
956
@login_required
957
def group_leave(request, group_id):
958
    try:
959
        m = Membership.objects.select_related().get(
960
            group__id=group_id,
961
            person=request.user
962
        )
963
    except Membership.DoesNotExist:
964
        return HttpResponseBadRequest(_('Invalid membership.'))
965
    if request.user in m.group.owner.all():
966
        return HttpResponseForbidden(_('Owner can not leave the group.'))
967
    return delete_object(
968
        request,
969
        model=Membership,
970
        object_id=m.id,
971
        template_name='im/astakosgroup_list.html',
972
        post_delete_redirect=reverse(
973
            'group_detail',
974
            kwargs=dict(group_id=group_id)
975
        )
976
    )
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
            )
987
        except Membership.DoesNotExist:
988
            return HttpResponseBadRequest(_('Invalid membership.'))
989
        else:
990
            if request.user not in m.group.owner.all():
991
                return HttpResponseForbidden(_('User is not a group owner.'))
992
            func(request, m)
993
            return render_response(
994
                template='im/astakosgroup_detail.html',
995
                context_instance=get_context(request),
996
                object=m.group,
997
                quota=m.group.quota
998
            )
999
    return wrapper
1000

    
1001

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

    
1016

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

    
1031

    
1032

    
1033

    
1034
@signed_terms_required
1035
@login_required
1036
def add_members(request, group_id):
1037
    if request.method != 'POST':
1038
        return HttpResponseBadRequest(_('Bad method'))
1039
    try:
1040
        group = AstakosGroup.objects.select_related().get(id=group_id)
1041
    except AstakosGroup.DoesNotExist:
1042
        return HttpResponseBadRequest(_('Invalid group.'))
1043
    search_form = AddGroupMembersForm(request.POST)
1044
    if search_form.is_valid():
1045
        users = search_form.get_valid_users()
1046
        map(group.approve_member, users)
1047
        search_form = AddGroupMembersForm()
1048
    form = AstakosGroupUpdateForm(instance=group)
1049
    return object_detail(request,
1050
                         AstakosGroup.objects.all(),
1051
                         object_id=group_id,
1052
                         extra_context={'quota': group.quota,
1053
                                        'form': form,
1054
                                        'search_form' : search_form})
1055

    
1056

    
1057
@signed_terms_required
1058
@login_required
1059
def resource_list(request):
1060
    return render_response(
1061
        template='im/astakosuserquota_list.html',
1062
        context_instance=get_context(request),
1063
        quota=request.user.quota
1064
    )
1065

    
1066

    
1067
def group_create_list(request):
1068
    return render_response(
1069
        template='im/astakosgroup_create_list.html',
1070
        context_instance=get_context(request),
1071
    )
1072

    
1073

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