Addcredits
[astakos] / snf-astakos-app / astakos / im / views.py
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
41 from django.contrib import messages
42 from django.contrib.auth.decorators import login_required
43 from django.contrib.auth.views import password_change
44 from django.core.urlresolvers import reverse
45 from django.db import transaction
46 from django.db.models import Q
47 from django.db.utils import IntegrityError
48 from django.forms.fields import URLField
49 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
50     HttpResponseRedirect, HttpResponseBadRequest
51 from django.shortcuts import redirect
52 from django.template import RequestContext, loader
53 from django.utils.http import urlencode
54 from django.utils.translation import ugettext as _
55 from django.views.generic.create_update import (create_object, delete_object,
56                                                 get_model_and_form_class)
57 from django.views.generic.list_detail import object_list, object_detail
58 from django.http import HttpResponseBadRequest
59
60 from astakos.im.models import (
61     AstakosUser, ApprovalTerms, AstakosGroup, Resource,
62     EmailChange, GroupKind, Membership)
63 from astakos.im.activation_backends import get_backend, SimpleBackend
64 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
65 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
66                               FeedbackForm, SignApprovalTermsForm,
67                               ExtendedPasswordChangeForm, EmailChangeForm,
68                               AstakosGroupCreationForm, AstakosGroupSearchForm,
69                               AstakosGroupUpdateForm, AddGroupMembersForm)
70 from astakos.im.functions import (send_feedback, SendMailError,
71                                   invite as invite_func, logout as auth_logout,
72                                   activate as activate_func,
73                                   switch_account_to_shibboleth,
74                                   send_admin_notification,
75                                   SendNotificationError)
76 from astakos.im.settings import (
77     COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
78     LOGGING_LEVEL
79 )
80 from astakos.im.tasks import request_billing
81
82 logger = logging.getLogger(__name__)
83
84
85 def render_response(template, tab=None, status=200, reset_cookie=False,
86                     context_instance=None, **kwargs):
87     """
88     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
89     keyword argument and returns an ``django.http.HttpResponse`` with the
90     specified ``status``.
91     """
92     if tab is None:
93         tab = template.partition('_')[0].partition('.html')[0]
94     kwargs.setdefault('tab', tab)
95     html = loader.render_to_string(
96         template, kwargs, context_instance=context_instance)
97     response = HttpResponse(html, status=status)
98     if reset_cookie:
99         set_cookie(response, context_instance['request'].user)
100     return response
101
102
103 def requires_anonymous(func):
104     """
105     Decorator checkes whether the request.user is not Anonymous and in that case
106     redirects to `logout`.
107     """
108     @wraps(func)
109     def wrapper(request, *args):
110         if not request.user.is_anonymous():
111             next = urlencode({'next': request.build_absolute_uri()})
112             logout_uri = reverse(logout) + '?' + next
113             return HttpResponseRedirect(logout_uri)
114         return func(request, *args)
115     return wrapper
116
117
118 def signed_terms_required(func):
119     """
120     Decorator checkes whether the request.user is Anonymous and in that case
121     redirects to `logout`.
122     """
123     @wraps(func)
124     def wrapper(request, *args, **kwargs):
125         if request.user.is_authenticated() and not request.user.signed_terms:
126             params = urlencode({'next': request.build_absolute_uri(),
127                                 'show_form': ''})
128             terms_uri = reverse('latest_terms') + '?' + params
129             return HttpResponseRedirect(terms_uri)
130         return func(request, *args, **kwargs)
131     return wrapper
132
133
134 @signed_terms_required
135 def index(request, login_template_name='im/login.html', extra_context=None):
136     """
137     If there is logged on user renders the profile page otherwise renders login page.
138
139     **Arguments**
140
141     ``login_template_name``
142         A custom login template to use. This is optional; if not specified,
143         this will default to ``im/login.html``.
144
145     ``profile_template_name``
146         A custom profile template to use. This is optional; if not specified,
147         this will default to ``im/profile.html``.
148
149     ``extra_context``
150         An dictionary of variables to add to the template context.
151
152     **Template:**
153
154     im/profile.html or im/login.html or ``template_name`` keyword argument.
155
156     """
157     template_name = login_template_name
158     if request.user.is_authenticated():
159         return HttpResponseRedirect(reverse('edit_profile'))
160     return render_response(template_name,
161                            login_form=LoginForm(request=request),
162                            context_instance=get_context(request, extra_context))
163
164
165 @login_required
166 @signed_terms_required
167 @transaction.commit_manually
168 def invite(request, template_name='im/invitations.html', extra_context=None):
169     """
170     Allows a user to invite somebody else.
171
172     In case of GET request renders a form for providing the invitee information.
173     In case of POST checks whether the user has not run out of invitations and then
174     sends an invitation email to singup to the service.
175
176     The view uses commit_manually decorator in order to ensure the number of the
177     user invitations is going to be updated only if the email has been successfully sent.
178
179     If the user isn't logged in, redirects to settings.LOGIN_URL.
180
181     **Arguments**
182
183     ``template_name``
184         A custom template to use. This is optional; if not specified,
185         this will default to ``im/invitations.html``.
186
187     ``extra_context``
188         An dictionary of variables to add to the template context.
189
190     **Template:**
191
192     im/invitations.html or ``template_name`` keyword argument.
193
194     **Settings:**
195
196     The view expectes the following settings are defined:
197
198     * LOGIN_URL: login uri
199     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
200     """
201     status = None
202     message = None
203     form = InvitationForm()
204
205     inviter = request.user
206     if request.method == 'POST':
207         form = InvitationForm(request.POST)
208         if inviter.invitations > 0:
209             if form.is_valid():
210                 try:
211                     invitation = form.save()
212                     invite_func(invitation, inviter)
213                     message = _('Invitation sent to %s' % invitation.username)
214                     messages.success(request, message)
215                 except SendMailError, e:
216                     message = e.message
217                     messages.error(request, message)
218                     transaction.rollback()
219                 except BaseException, e:
220                     message = _('Something went wrong.')
221                     messages.error(request, message)
222                     logger.exception(e)
223                     transaction.rollback()
224                 else:
225                     transaction.commit()
226         else:
227             message = _('No invitations left')
228             messages.error(request, message)
229
230     sent = [{'email': inv.username,
231              'realname': inv.realname,
232              'is_consumed': inv.is_consumed}
233             for inv in request.user.invitations_sent.all()]
234     kwargs = {'inviter': inviter,
235               'sent': sent}
236     context = get_context(request, extra_context, **kwargs)
237     return render_response(template_name,
238                            invitation_form=form,
239                            context_instance=context)
240
241
242 @login_required
243 @signed_terms_required
244 def edit_profile(request, template_name='im/profile.html', extra_context=None):
245     """
246     Allows a user to edit his/her profile.
247
248     In case of GET request renders a form for displaying the user information.
249     In case of POST updates the user informantion and redirects to ``next``
250     url parameter if exists.
251
252     If the user isn't logged in, redirects to settings.LOGIN_URL.
253
254     **Arguments**
255
256     ``template_name``
257         A custom template to use. This is optional; if not specified,
258         this will default to ``im/profile.html``.
259
260     ``extra_context``
261         An dictionary of variables to add to the template context.
262
263     **Template:**
264
265     im/profile.html or ``template_name`` keyword argument.
266
267     **Settings:**
268
269     The view expectes the following settings are defined:
270
271     * LOGIN_URL: login uri
272     """
273     extra_context = extra_context or {}
274     form = ProfileForm(instance=request.user)
275     extra_context['next'] = request.GET.get('next')
276     reset_cookie = False
277     if request.method == 'POST':
278         form = ProfileForm(request.POST, instance=request.user)
279         if form.is_valid():
280             try:
281                 prev_token = request.user.auth_token
282                 user = form.save()
283                 reset_cookie = user.auth_token != prev_token
284                 form = ProfileForm(instance=user)
285                 next = request.POST.get('next')
286                 if next:
287                     return redirect(next)
288                 msg = _('Profile has been updated successfully')
289                 messages.success(request, msg)
290             except ValueError, ve:
291                 messages.success(request, ve)
292     elif request.method == "GET":
293         if not request.user.is_verified:
294             request.user.is_verified = True
295             request.user.save()
296     return render_response(template_name,
297                            reset_cookie=reset_cookie,
298                            profile_form=form,
299                            context_instance=get_context(request,
300                                                         extra_context))
301
302
303 @transaction.commit_manually
304 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
305     """
306     Allows a user to create a local account.
307
308     In case of GET request renders a form for entering the user information.
309     In case of POST handles the signup.
310
311     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
312     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
313     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
314     (see activation_backends);
315
316     Upon successful user creation, if ``next`` url parameter is present the user is redirected there
317     otherwise renders the same page with a success message.
318
319     On unsuccessful creation, renders ``template_name`` with an error message.
320
321     **Arguments**
322
323     ``template_name``
324         A custom template to render. This is optional;
325         if not specified, this will default to ``im/signup.html``.
326
327     ``on_success``
328         A custom template to render in case of success. This is optional;
329         if not specified, this will default to ``im/signup_complete.html``.
330
331     ``extra_context``
332         An dictionary of variables to add to the template context.
333
334     **Template:**
335
336     im/signup.html or ``template_name`` keyword argument.
337     im/signup_complete.html or ``on_success`` keyword argument.
338     """
339     if request.user.is_authenticated():
340         return HttpResponseRedirect(reverse('edit_profile'))
341
342     provider = get_query(request).get('provider', 'local')
343     try:
344         if not backend:
345             backend = get_backend(request)
346         form = backend.get_signup_form(provider)
347     except Exception, e:
348         form = SimpleBackend(request).get_signup_form(provider)
349         messages.error(request, e)
350     if request.method == 'POST':
351         if form.is_valid():
352             user = form.save(commit=False)
353             try:
354                 result = backend.handle_activation(user)
355                 status = messages.SUCCESS
356                 message = result.message
357                 user.save()
358                 if 'additional_email' in form.cleaned_data:
359                     additional_email = form.cleaned_data['additional_email']
360                     if additional_email != user.email:
361                         user.additionalmail_set.create(email=additional_email)
362                         msg = 'Additional email: %s saved for user %s.' % (
363                             additional_email, user.email)
364                         logger.log(LOGGING_LEVEL, msg)
365                 if user and user.is_active:
366                     next = request.POST.get('next', '')
367                     return prepare_response(request, user, next=next)
368                 messages.add_message(request, status, message)
369                 return render_response(on_success,
370                                        context_instance=get_context(request, extra_context))
371             except SendMailError, e:
372                 message = e.message
373                 messages.error(request, message)
374                 transaction.rollback()
375             except BaseException, e:
376                 message = _('Something went wrong.')
377                 messages.error(request, message)
378                 logger.exception(e)
379                 transaction.rollback()
380             else:
381                 transaction.commit()
382     return render_response(template_name,
383                            signup_form=form,
384                            provider=provider,
385                            context_instance=get_context(request, extra_context))
386
387
388 @login_required
389 @signed_terms_required
390 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
391     """
392     Allows a user to send feedback.
393
394     In case of GET request renders a form for providing the feedback information.
395     In case of POST sends an email to support team.
396
397     If the user isn't logged in, redirects to settings.LOGIN_URL.
398
399     **Arguments**
400
401     ``template_name``
402         A custom template to use. This is optional; if not specified,
403         this will default to ``im/feedback.html``.
404
405     ``extra_context``
406         An dictionary of variables to add to the template context.
407
408     **Template:**
409
410     im/signup.html or ``template_name`` keyword argument.
411
412     **Settings:**
413
414     * LOGIN_URL: login uri
415     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
416     """
417     if request.method == 'GET':
418         form = FeedbackForm()
419     if request.method == 'POST':
420         if not request.user:
421             return HttpResponse('Unauthorized', status=401)
422
423         form = FeedbackForm(request.POST)
424         if form.is_valid():
425             msg = form.cleaned_data['feedback_msg']
426             data = form.cleaned_data['feedback_data']
427             try:
428                 send_feedback(msg, data, request.user, email_template_name)
429             except SendMailError, e:
430                 messages.error(request, message)
431             else:
432                 message = _('Feedback successfully sent')
433                 messages.success(request, message)
434     return render_response(template_name,
435                            feedback_form=form,
436                            context_instance=get_context(request, extra_context))
437
438
439 @signed_terms_required
440 def logout(request, template='registration/logged_out.html', extra_context=None):
441     """
442     Wraps `django.contrib.auth.logout` and delete the cookie.
443     """
444     response = HttpResponse()
445     if request.user.is_authenticated():
446         email = request.user.email
447         auth_logout(request)
448         response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
449         msg = 'Cookie deleted for %s' % email
450         logger.log(LOGGING_LEVEL, msg)
451     next = request.GET.get('next')
452     if next:
453         response['Location'] = next
454         response.status_code = 302
455         return response
456     elif LOGOUT_NEXT:
457         response['Location'] = LOGOUT_NEXT
458         response.status_code = 301
459         return response
460     messages.success(request, _('You have successfully logged out.'))
461     context = get_context(request, extra_context)
462     response.write(loader.render_to_string(template, context_instance=context))
463     return response
464
465
466 @transaction.commit_manually
467 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
468     """
469     Activates the user identified by the ``auth`` request parameter, sends a welcome email
470     and renews the user token.
471
472     The view uses commit_manually decorator in order to ensure the user state will be updated
473     only if the email will be send successfully.
474     """
475     token = request.GET.get('auth')
476     next = request.GET.get('next')
477     try:
478         user = AstakosUser.objects.get(auth_token=token)
479     except AstakosUser.DoesNotExist:
480         return HttpResponseBadRequest(_('No such user'))
481
482     if user.is_active:
483         message = _('Account already active.')
484         messages.error(request, message)
485         return index(request)
486
487     try:
488         local_user = AstakosUser.objects.get(
489             ~Q(id=user.id),
490             email=user.email,
491             is_active=True
492         )
493     except AstakosUser.DoesNotExist:
494         try:
495             activate_func(
496                 user,
497                 greeting_email_template_name,
498                 helpdesk_email_template_name,
499                 verify_email=True
500             )
501             response = prepare_response(request, user, next, renew=True)
502             transaction.commit()
503             return response
504         except SendMailError, e:
505             message = e.message
506             messages.error(request, message)
507             transaction.rollback()
508             return index(request)
509         except BaseException, e:
510             message = _('Something went wrong.')
511             messages.error(request, message)
512             logger.exception(e)
513             transaction.rollback()
514             return index(request)
515     else:
516         try:
517             user = switch_account_to_shibboleth(
518                 user,
519                 local_user,
520                 greeting_email_template_name
521             )
522             response = prepare_response(request, user, next, renew=True)
523             transaction.commit()
524             return response
525         except SendMailError, e:
526             message = e.message
527             messages.error(request, message)
528             transaction.rollback()
529             return index(request)
530         except BaseException, e:
531             message = _('Something went wrong.')
532             messages.error(request, message)
533             logger.exception(e)
534             transaction.rollback()
535             return index(request)
536
537
538 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
539     term = None
540     terms = None
541     if not term_id:
542         try:
543             term = ApprovalTerms.objects.order_by('-id')[0]
544         except IndexError:
545             pass
546     else:
547         try:
548             term = ApprovalTerms.objects.get(id=term_id)
549         except ApprovalTerms.DoesNotExist, e:
550             pass
551
552     if not term:
553         return HttpResponseRedirect(reverse('index'))
554     f = open(term.location, 'r')
555     terms = f.read()
556
557     if request.method == 'POST':
558         next = request.POST.get('next')
559         if not next:
560             next = reverse('index')
561         form = SignApprovalTermsForm(request.POST, instance=request.user)
562         if not form.is_valid():
563             return render_response(template_name,
564                                    terms=terms,
565                                    approval_terms_form=form,
566                                    context_instance=get_context(request, extra_context))
567         user = form.save()
568         return HttpResponseRedirect(next)
569     else:
570         form = None
571         if request.user.is_authenticated() and not request.user.signed_terms:
572             form = SignApprovalTermsForm(instance=request.user)
573         return render_response(template_name,
574                                terms=terms,
575                                approval_terms_form=form,
576                                context_instance=get_context(request, extra_context))
577
578
579 @signed_terms_required
580 def change_password(request):
581     return password_change(request,
582                            post_change_redirect=reverse('edit_profile'),
583                            password_change_form=ExtendedPasswordChangeForm)
584
585
586 @signed_terms_required
587 @login_required
588 @transaction.commit_manually
589 def change_email(request, activation_key=None,
590                  email_template_name='registration/email_change_email.txt',
591                  form_template_name='registration/email_change_form.html',
592                  confirm_template_name='registration/email_change_done.html',
593                  extra_context=None):
594     if activation_key:
595         try:
596             user = EmailChange.objects.change_email(activation_key)
597             if request.user.is_authenticated() and request.user == user:
598                 msg = _('Email changed successfully.')
599                 messages.success(request, msg)
600                 auth_logout(request)
601                 response = prepare_response(request, user)
602                 transaction.commit()
603                 return response
604         except ValueError, e:
605             messages.error(request, e)
606         return render_response(confirm_template_name,
607                                modified_user=user if 'user' in locals(
608                                ) else None,
609                                context_instance=get_context(request,
610                                                             extra_context))
611
612     if not request.user.is_authenticated():
613         path = quote(request.get_full_path())
614         url = request.build_absolute_uri(reverse('index'))
615         return HttpResponseRedirect(url + '?next=' + path)
616     form = EmailChangeForm(request.POST or None)
617     if request.method == 'POST' and form.is_valid():
618         try:
619             ec = form.save(email_template_name, request)
620         except SendMailError, e:
621             msg = e
622             messages.error(request, msg)
623             transaction.rollback()
624         except IntegrityError, e:
625             msg = _('There is already a pending change email request.')
626             messages.error(request, msg)
627         else:
628             msg = _('Change email request has been registered succefully.\
629                     You are going to receive a verification email in the new address.')
630             messages.success(request, msg)
631             transaction.commit()
632     return render_response(form_template_name,
633                            form=form,
634                            context_instance=get_context(request,
635                                                         extra_context))
636
637
638 @signed_terms_required
639 @login_required
640 def group_add(request, kind_name='default'):
641     try:
642         kind = GroupKind.objects.get(name=kind_name)
643     except:
644         return HttpResponseBadRequest(_('No such group kind'))
645
646     template_loader = loader
647     post_save_redirect = '/im/group/%(id)s/'
648     context_processors = None
649     model, form_class = get_model_and_form_class(
650         model=None,
651         form_class=AstakosGroupCreationForm
652     )
653     resources = dict(
654         (str(r.id), r) for r in Resource.objects.select_related().all())
655     policies = []
656     if request.method == 'POST':
657         form = form_class(request.POST, request.FILES, resources=resources)
658         if form.is_valid():
659             new_object = form.save()
660
661             # save owner
662             new_object.owners = [request.user]
663
664             # save quota policies
665             for (rid, limit) in form.resources():
666                 try:
667                     r = resources[rid]
668                 except KeyError, e:
669                     logger.exception(e)
670                     # TODO Should I stay or should I go???
671                     continue
672                 else:
673                     new_object.astakosgroupquota_set.create(
674                         resource=r,
675                         limit=limit
676                     )
677                 policies.append('%s %d' % (r, limit))
678             msg = _("The %(verbose_name)s was created successfully.") %\
679                 {"verbose_name": model._meta.verbose_name}
680             messages.success(request, msg, fail_silently=True)
681
682             # send notification
683             try:
684                 send_admin_notification(
685                     template_name='im/group_creation_notification.txt',
686                     dictionary={
687                         'group': new_object,
688                         'owner': request.user,
689                         'policies': policies,
690                     },
691                     subject='%s alpha2 testing group creation notification' % SITENAME
692                 )
693             except SendNotificationError, e:
694                 messages.error(request, e, fail_silently=True)
695             return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
696     else:
697         now = datetime.now()
698         data = {
699             'kind': kind
700         }
701         form = form_class(data, resources=resources)
702
703     # Create the template, context, response
704     template_name = "%s/%s_form.html" % (
705         model._meta.app_label,
706         model._meta.object_name.lower()
707     )
708     t = template_loader.get_template(template_name)
709     c = RequestContext(request, {
710         'form': form,
711         'kind': kind,
712     }, context_processors)
713     return HttpResponse(t.render(c))
714
715
716 @signed_terms_required
717 @login_required
718 def group_list(request):
719     list = request.user.astakos_groups.select_related().all()
720     return object_list(request, queryset=list,
721                        extra_context=dict(
722                        is_search=False
723                        )
724                        )
725
726
727 @signed_terms_required
728 @login_required
729 def group_detail(request, group_id):
730     try:
731         group = AstakosGroup.objects.select_related().get(id=group_id)
732     except AstakosGroup.DoesNotExist:
733         return HttpResponseBadRequest(_('Invalid group.'))
734     form = AstakosGroupUpdateForm(instance=group)
735     search_form = AddGroupMembersForm()
736     return object_detail(request,
737                          AstakosGroup.objects.all(),
738                          object_id=group_id,
739                          extra_context={'quota': group.quota,
740                                         'form': form,
741                                         'search_form': search_form}
742                          )
743
744
745 @signed_terms_required
746 @login_required
747 def group_update(request, group_id):
748     if request.method != 'POST':
749         return HttpResponseBadRequest('Method not allowed.')
750     try:
751         group = AstakosGroup.objects.select_related().get(id=group_id)
752     except AstakosGroup.DoesNotExist:
753         return HttpResponseBadRequest(_('Invalid group.'))
754     form = AstakosGroupUpdateForm(request.POST, instance=group)
755     if form.is_valid():
756         form.save()
757     search_form = AddGroupMembersForm()
758     return object_detail(request,
759                          AstakosGroup.objects.all(),
760                          object_id=group_id,
761                          extra_context={'quota': group.quota,
762                                         'form': form,
763                                         'search_form': search_form})
764
765 @signed_terms_required
766 @login_required
767 def group_search(request, extra_context=None, **kwargs):
768     if request.method == 'GET':
769         form = AstakosGroupSearchForm()
770     else:
771         form = AstakosGroupSearchForm(get_query(request))
772         if form.is_valid():
773             q = form.cleaned_data['q'].strip()
774             queryset = AstakosGroup.objects.select_related(
775             ).filter(name__contains=q)
776             return object_list(
777                 request,
778                 queryset,
779                 template_name='im/astakosgroup_list.html',
780                 extra_context=dict(form=form,
781                                    is_search=True))
782     return render_response(
783         template='im/astakosgroup_list.html',
784         form=form,
785         context_instance=get_context(request, extra_context),
786         is_search=False
787     )
788
789 @signed_terms_required
790 @login_required
791 def group_all(request, extra_context=None, **kwargs):
792     if request.method != 'POST':
793         return HttpResponseBadRequest(_('Bad method'))
794     return object_list(
795                 request,
796                 AstakosGroup.objects.select_related().all(),
797                 template_name='im/astakosgroup_list.html',
798                 extra_context=dict(form=AstakosGroupSearchForm(),
799                                    is_search=True))
800
801
802 @signed_terms_required
803 @login_required
804 def group_join(request, group_id):
805     m = Membership(group_id=group_id,
806                    person=request.user,
807                    date_requested=datetime.now()
808                    )
809     try:
810         m.save()
811         post_save_redirect = reverse(
812             'group_detail',
813             kwargs=dict(group_id=group_id)
814         )
815         return HttpResponseRedirect(post_save_redirect)
816     except IntegrityError, e:
817         logger.exception(e)
818         msg = _('Failed to join group.')
819         messages.error(request, msg)
820         return group_search(request)
821
822
823 @signed_terms_required
824 @login_required
825 def group_leave(request, group_id):
826     try:
827         m = Membership.objects.select_related().get(
828             group__id=group_id,
829             person=request.user
830         )
831     except Membership.DoesNotExist:
832         return HttpResponseBadRequest(_('Invalid membership.'))
833     if request.user in m.group.owner.all():
834         return HttpResponseForbidden(_('Owner can not leave the group.'))
835     return delete_object(
836         request,
837         model=Membership,
838         object_id=m.id,
839         template_name='im/astakosgroup_list.html',
840         post_delete_redirect=reverse(
841             'group_detail',
842             kwargs=dict(group_id=group_id)
843         )
844     )
845
846
847 def handle_membership(func):
848     @wraps(func)
849     def wrapper(request, group_id, user_id):
850         try:
851             m = Membership.objects.select_related().get(
852                 group__id=group_id,
853                 person__id=user_id
854             )
855         except Membership.DoesNotExist:
856             return HttpResponseBadRequest(_('Invalid membership.'))
857         else:
858             if request.user not in m.group.owner.all():
859                 return HttpResponseForbidden(_('User is not a group owner.'))
860             func(request, m)
861             return render_response(
862                 template='im/astakosgroup_detail.html',
863                 context_instance=get_context(request),
864                 object=m.group,
865                 quota=m.group.quota
866             )
867     return wrapper
868
869
870 @signed_terms_required
871 @login_required
872 @handle_membership
873 def approve_member(request, membership):
874     try:
875         membership.approve()
876         realname = membership.person.realname
877         msg = _('%s has been successfully joined the group.' % realname)
878         messages.success(request, msg)
879     except BaseException, e:
880         logger.exception(e)
881         msg = _('Something went wrong during %s\'s approval.' % realname)
882         messages.error(request, msg)
883
884
885 @signed_terms_required
886 @login_required
887 @handle_membership
888 def disapprove_member(request, membership):
889     try:
890         membership.disapprove()
891         realname = membership.person.realname
892         msg = _('%s has been successfully removed from the group.' % realname)
893         messages.success(request, msg)
894     except BaseException, e:
895         logger.exception(e)
896         msg = _('Something went wrong during %s\'s disapproval.' % realname)
897         messages.error(request, msg)
898
899
900
901
902 @signed_terms_required
903 @login_required
904 def add_members(request, group_id):
905     if request.method != 'POST':
906         return HttpResponseBadRequest(_('Bad method'))
907     try:
908         group = AstakosGroup.objects.select_related().get(id=group_id)
909     except AstakosGroup.DoesNotExist:
910         return HttpResponseBadRequest(_('Invalid group.'))
911     search_form = AddGroupMembersForm(request.POST)
912     if search_form.is_valid():
913         users = search_form.get_valid_users()
914         map(group.approve_member, users)
915         search_form = AddGroupMembersForm()
916     form = AstakosGroupUpdateForm(instance=group)
917     return object_detail(request,
918                          AstakosGroup.objects.all(),
919                          object_id=group_id,
920                          extra_context={'quota': group.quota,
921                                         'form': form,
922                                         'search_form' : search_form}
923                          )
924
925
926 @signed_terms_required
927 @login_required
928 def resource_list(request):
929     return render_response(
930         template='im/astakosuserquota_list.html',
931         context_instance=get_context(request),
932         quota=request.user.quota
933     )
934
935
936 def group_create_list(request):
937     return render_response(
938         template='im/astakosgroup_create_list.html',
939         context_instance=get_context(request),
940     )
941
942
943 @signed_terms_required
944 @login_required
945 def billing(request):
946     
947     today = datetime.today()
948     month_last_day= calendar.monthrange(today.year, today.month)[1]
949     
950     start = request.POST.get('datefrom', None)
951     if start:
952         today = datetime.fromtimestamp(int(start))
953         month_last_day= calendar.monthrange(today.year, today.month)[1]
954     
955     start = datetime(today.year, today.month, 1).strftime("%s")
956     end = datetime(today.year, today.month, month_last_day).strftime("%s")
957     r = request_billing.apply(args=('pgerakios@grnet.gr',
958                                     int(start) * 1000,
959                                     int(end) * 1000))
960     data = {}
961     
962     try:
963         status, data = r.result
964         data=clear_billing_data(data)
965         if status != 200:
966             messages.error(request, _('Service response status: %d' % status))
967     except:
968         messages.error(request, r.result)
969     
970     print type(start)
971     
972     return render_response(
973         template='im/billing.html',
974         context_instance=get_context(request),
975         data=data,
976         zerodate=datetime(month=1,year=1970, day=1),
977         today=today,
978         start=int(start),
979         month_last_day=month_last_day)  
980     
981 def clear_billing_data(data):
982     
983     # remove addcredits entries
984     def isnotcredit(e):
985         return e['serviceName'] != "addcredits"
986     
987     
988     
989     # separate services    
990     def servicefilter(service_name):
991         service = service_name
992         def fltr(e):
993             return e['serviceName'] == service
994         return fltr
995         
996     
997     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
998     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
999     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1000     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1001         
1002     return data