Fix pagination in search group view
[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 from astakos.im.functions import (send_feedback, SendMailError,
72                                   invite as invite_func, logout as auth_logout,
73                                   activate as activate_func,
74                                   switch_account_to_shibboleth,
75                                   send_admin_notification,
76                                   SendNotificationError)
77 from astakos.im.settings import (
78     COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
79     LOGGING_LEVEL, PAGINATE_BY)
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                     transaction.commit()
368                     return prepare_response(request, user, next=next)
369                 messages.add_message(request, status, message)
370                 transaction.commit()
371                 return render_response(on_success,
372                                        context_instance=get_context(request, extra_context))
373             except SendMailError, e:
374                 message = e.message
375                 messages.error(request, message)
376                 transaction.rollback()
377             except BaseException, e:
378                 message = _('Something went wrong.')
379                 messages.error(request, message)
380                 logger.exception(e)
381                 transaction.rollback()
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, uplimit) 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                         uplimit=uplimit
676                     )
677                 policies.append('%s %d' % (r, uplimit))
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     q = request.user.astakos_groups.none()
720     list = request.user.astakos_groups.select_related().all()
721     d = {}
722     d['own'] = [g for g in list if request.user in g.owner.all()]
723     d['other'] = list.exclude(id__in=(g.id for g in d['own']))
724     for k, queryset in d.iteritems():
725         paginator = Paginator(queryset, PAGINATE_BY)
726         page = request.GET.get('%s_page' % k, 1)
727         try:
728             page_number = int(page)
729         except ValueError:
730             if page == 'last':
731                 page_number = paginator.num_pages
732             else:
733                 # Page is not 'last', nor can it be converted to an int.
734                 raise Http404
735         try:
736             page_obj = locals()['%s_page_obj' % k] = paginator.page(page_number)
737         except InvalidPage:
738             raise Http404
739     return object_list(request, queryset=q,
740                        extra_context={'is_search':False,
741                                       'mine': locals()['own_page_obj'],
742                                       'other': locals()['other_page_obj']})
743
744
745 @signed_terms_required
746 @login_required
747 def group_detail(request, group_id):
748     try:
749         group = AstakosGroup.objects.select_related().get(id=group_id)
750     except AstakosGroup.DoesNotExist:
751         return HttpResponseBadRequest(_('Invalid group.'))
752     form = AstakosGroupUpdateForm(instance=group)
753     search_form = AddGroupMembersForm()
754     return object_detail(request,
755                          AstakosGroup.objects.all(),
756                          object_id=group_id,
757                          extra_context={'quota': group.quota,
758                                         'form': form,
759                                         'search_form': search_form}
760                          )
761
762
763 @signed_terms_required
764 @login_required
765 def group_update(request, group_id):
766     if request.method != 'POST':
767         return HttpResponseBadRequest('Method not allowed.')
768     try:
769         group = AstakosGroup.objects.select_related().get(id=group_id)
770     except AstakosGroup.DoesNotExist:
771         return HttpResponseBadRequest(_('Invalid group.'))
772     form = AstakosGroupUpdateForm(request.POST, instance=group)
773     if form.is_valid():
774         form.save()
775     search_form = AddGroupMembersForm()
776     return object_detail(request,
777                          AstakosGroup.objects.all(),
778                          object_id=group_id,
779                          extra_context={'quota': group.quota,
780                                         'form': form,
781                                         'search_form': search_form})
782
783 @signed_terms_required
784 @login_required
785 def group_search(request, extra_context=None, **kwargs):
786     q = request.GET.get('q')
787     if request.method == 'GET':
788         form = AstakosGroupSearchForm({'q': q} if q else None)
789     else:
790         form = AstakosGroupSearchForm(get_query(request))
791         if form.is_valid():
792             q = form.cleaned_data['q'].strip()
793     if q:
794         queryset = AstakosGroup.objects.select_related(
795         ).filter(name__contains=q).filter(approval_date__isnull=False)
796     else:
797         queryset = AstakosGroup.objects.none()
798     return object_list(
799         request,
800         queryset,
801         paginate_by=PAGINATE_BY,
802         page=request.GET.get('page') or 1,
803         template_name='im/astakosgroup_list.html',
804         extra_context=dict(form=form,
805                            is_search=True,
806                            q=q))
807
808 @signed_terms_required
809 @login_required
810 def group_all(request, extra_context=None, **kwargs):
811     return object_list(
812                 request,
813                 AstakosGroup.objects.select_related().filter(
814                     approval_date__isnull=False),
815                 paginate_by=PAGINATE_BY,
816                 page=request.GET.get('page') or 1,
817                 template_name='im/astakosgroup_list.html',
818                 extra_context=dict(form=AstakosGroupSearchForm(),
819                                    is_search=True))
820
821
822 @signed_terms_required
823 @login_required
824 def group_join(request, group_id):
825     m = Membership(group_id=group_id,
826                    person=request.user,
827                    date_requested=datetime.now()
828                    )
829     try:
830         m.save()
831         post_save_redirect = reverse(
832             'group_detail',
833             kwargs=dict(group_id=group_id)
834         )
835         return HttpResponseRedirect(post_save_redirect)
836     except IntegrityError, e:
837         logger.exception(e)
838         msg = _('Failed to join group.')
839         messages.error(request, msg)
840         return group_search(request)
841
842
843 @signed_terms_required
844 @login_required
845 def group_leave(request, group_id):
846     try:
847         m = Membership.objects.select_related().get(
848             group__id=group_id,
849             person=request.user
850         )
851     except Membership.DoesNotExist:
852         return HttpResponseBadRequest(_('Invalid membership.'))
853     if request.user in m.group.owner.all():
854         return HttpResponseForbidden(_('Owner can not leave the group.'))
855     return delete_object(
856         request,
857         model=Membership,
858         object_id=m.id,
859         template_name='im/astakosgroup_list.html',
860         post_delete_redirect=reverse(
861             'group_detail',
862             kwargs=dict(group_id=group_id)
863         )
864     )
865
866
867 def handle_membership(func):
868     @wraps(func)
869     def wrapper(request, group_id, user_id):
870         try:
871             m = Membership.objects.select_related().get(
872                 group__id=group_id,
873                 person__id=user_id
874             )
875         except Membership.DoesNotExist:
876             return HttpResponseBadRequest(_('Invalid membership.'))
877         else:
878             if request.user not in m.group.owner.all():
879                 return HttpResponseForbidden(_('User is not a group owner.'))
880             func(request, m)
881             return render_response(
882                 template='im/astakosgroup_detail.html',
883                 context_instance=get_context(request),
884                 object=m.group,
885                 quota=m.group.quota
886             )
887     return wrapper
888
889
890 @signed_terms_required
891 @login_required
892 @handle_membership
893 def approve_member(request, membership):
894     try:
895         membership.approve()
896         realname = membership.person.realname
897         msg = _('%s has been successfully joined the group.' % realname)
898         messages.success(request, msg)
899     except BaseException, e:
900         logger.exception(e)
901         msg = _('Something went wrong during %s\'s approval.' % realname)
902         messages.error(request, msg)
903
904
905 @signed_terms_required
906 @login_required
907 @handle_membership
908 def disapprove_member(request, membership):
909     try:
910         membership.disapprove()
911         realname = membership.person.realname
912         msg = _('%s has been successfully removed from the group.' % realname)
913         messages.success(request, msg)
914     except BaseException, e:
915         logger.exception(e)
916         msg = _('Something went wrong during %s\'s disapproval.' % realname)
917         messages.error(request, msg)
918
919
920
921
922 @signed_terms_required
923 @login_required
924 def add_members(request, group_id):
925     if request.method != 'POST':
926         return HttpResponseBadRequest(_('Bad method'))
927     try:
928         group = AstakosGroup.objects.select_related().get(id=group_id)
929     except AstakosGroup.DoesNotExist:
930         return HttpResponseBadRequest(_('Invalid group.'))
931     search_form = AddGroupMembersForm(request.POST)
932     if search_form.is_valid():
933         users = search_form.get_valid_users()
934         map(group.approve_member, users)
935         search_form = AddGroupMembersForm()
936     form = AstakosGroupUpdateForm(instance=group)
937     return object_detail(request,
938                          AstakosGroup.objects.all(),
939                          object_id=group_id,
940                          extra_context={'quota': group.quota,
941                                         'form': form,
942                                         'search_form' : search_form}
943                          )
944
945
946 @signed_terms_required
947 @login_required
948 def resource_list(request):
949     return render_response(
950         template='im/astakosuserquota_list.html',
951         context_instance=get_context(request),
952         quota=request.user.quota
953     )
954
955
956 def group_create_list(request):
957     return render_response(
958         template='im/astakosgroup_create_list.html',
959         context_instance=get_context(request),
960     )
961
962
963 @signed_terms_required
964 @login_required
965 def billing(request):
966     
967     today = datetime.today()
968     month_last_day= calendar.monthrange(today.year, today.month)[1]
969     
970     start = request.POST.get('datefrom', None)
971     if start:
972         today = datetime.fromtimestamp(int(start))
973         month_last_day= calendar.monthrange(today.year, today.month)[1]
974     
975     start = datetime(today.year, today.month, 1).strftime("%s")
976     end = datetime(today.year, today.month, month_last_day).strftime("%s")
977     r = request_billing.apply(args=('pgerakios@grnet.gr',
978                                     int(start) * 1000,
979                                     int(end) * 1000))
980     data = {}
981     
982     try:
983         status, data = r.result
984         data=clear_billing_data(data)
985         if status != 200:
986             messages.error(request, _('Service response status: %d' % status))
987     except:
988         messages.error(request, r.result)
989     
990     print type(start)
991     
992     return render_response(
993         template='im/billing.html',
994         context_instance=get_context(request),
995         data=data,
996         zerodate=datetime(month=1,year=1970, day=1),
997         today=today,
998         start=int(start),
999         month_last_day=month_last_day)  
1000     
1001 def clear_billing_data(data):
1002     
1003     # remove addcredits entries
1004     def isnotcredit(e):
1005         return e['serviceName'] != "addcredits"
1006     
1007     
1008     
1009     # separate services    
1010     def servicefilter(service_name):
1011         service = service_name
1012         def fltr(e):
1013             return e['serviceName'] == service
1014         return fltr
1015         
1016     
1017     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1018     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1019     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1020     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1021         
1022     return data