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