Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 701118f4

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 as template_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
from django.core.xheaders import populate_xheaders
63

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

    
86
logger = logging.getLogger(__name__)
87

    
88

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

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

    
109

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

    
124

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

    
140

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

146
    **Arguments**
147

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

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

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

159
    **Template:**
160

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

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

    
171

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

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

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

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

188
    **Arguments**
189

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

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

197
    **Template:**
198

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

201
    **Settings:**
202

203
    The view expectes the following settings are defined:
204

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

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

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

    
248

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

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

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

261
    **Arguments**
262

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

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

270
    **Template:**
271

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

274
    **Settings:**
275

276
    The view expectes the following settings are defined:
277

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

    
309

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

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

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

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

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

328
    **Arguments**
329

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

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

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

341
    **Template:**
342

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

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

    
394

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

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

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

406
    **Arguments**
407

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

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

415
    **Template:**
416

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

419
    **Settings:**
420

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

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

    
445

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

    
472

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

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

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

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

    
544

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

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

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

    
585

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

    
592

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

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

    
644

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

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

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

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

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

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

    
722

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

    
791

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

    
856

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

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

    
947

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

    
966

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

    
990

    
991
def handle_membership(func):
992
    @wraps(func)
993
    def wrapper(request, group_id, user_id):
994
        try:
995
            m = Membership.objects.select_related().get(
996
                group__id=group_id,
997
                person__id=user_id
998
            )
999
        except Membership.DoesNotExist:
1000
            return HttpResponseBadRequest(_('Invalid membership.'))
1001
        else:
1002
            if request.user not in m.group.owner.all():
1003
                return HttpResponseForbidden(_('User is not a group owner.'))
1004
            func(request, m)
1005
            return render_response(
1006
                template='im/astakosgroup_detail.html',
1007
                context_instance=get_context(request),
1008
                object=m.group,
1009
                quota=m.group.quota
1010
            )
1011
    return wrapper
1012

    
1013

    
1014
@signed_terms_required
1015
@login_required
1016
@handle_membership
1017
def approve_member(request, membership):
1018
    try:
1019
        membership.approve()
1020
        realname = membership.person.realname
1021
        msg = _('%s has been successfully joined the group.' % realname)
1022
        messages.success(request, msg)
1023
    except BaseException, e:
1024
        logger.exception(e)
1025
        msg = _('Something went wrong during %s\'s approval.' % realname)
1026
        messages.error(request, msg)
1027

    
1028

    
1029
@signed_terms_required
1030
@login_required
1031
@handle_membership
1032
def disapprove_member(request, membership):
1033
    try:
1034
        membership.disapprove()
1035
        realname = membership.person.realname
1036
        msg = _('%s has been successfully removed from the group.' % realname)
1037
        messages.success(request, msg)
1038
    except BaseException, e:
1039
        logger.exception(e)
1040
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
1041
        messages.error(request, msg)
1042

    
1043

    
1044
@signed_terms_required
1045
@login_required
1046
def resource_list(request):
1047
    return render_response(
1048
        template='im/astakosuserquota_list.html',
1049
        context_instance=get_context(request),
1050
        quota=request.user.quota
1051
    )
1052

    
1053

    
1054
def group_create_list(request):
1055
    return render_response(
1056
        template='im/astakosgroup_create_list.html',
1057
        context_instance=get_context(request),
1058
    )
1059

    
1060

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

    
1122

    
1123
def _get_default(field):
1124
    if isinstance(field, DateTimeField):
1125
        return datetime.utcfromtimestamp(0)
1126
    elif isinstance(field, int):
1127
        return 0
1128
    return ''