Add pagination
[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)
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().all(),
814                 paginate_by=PAGINATE_BY,
815                 page=request.GET.get('page') or 1,
816                 template_name='im/astakosgroup_list.html',
817                 extra_context=dict(form=AstakosGroupSearchForm(),
818                                    is_search=True))
819
820
821 @signed_terms_required
822 @login_required
823 def group_join(request, group_id):
824     m = Membership(group_id=group_id,
825                    person=request.user,
826                    date_requested=datetime.now()
827                    )
828     try:
829         m.save()
830         post_save_redirect = reverse(
831             'group_detail',
832             kwargs=dict(group_id=group_id)
833         )
834         return HttpResponseRedirect(post_save_redirect)
835     except IntegrityError, e:
836         logger.exception(e)
837         msg = _('Failed to join group.')
838         messages.error(request, msg)
839         return group_search(request)
840
841
842 @signed_terms_required
843 @login_required
844 def group_leave(request, group_id):
845     try:
846         m = Membership.objects.select_related().get(
847             group__id=group_id,
848             person=request.user
849         )
850     except Membership.DoesNotExist:
851         return HttpResponseBadRequest(_('Invalid membership.'))
852     if request.user in m.group.owner.all():
853         return HttpResponseForbidden(_('Owner can not leave the group.'))
854     return delete_object(
855         request,
856         model=Membership,
857         object_id=m.id,
858         template_name='im/astakosgroup_list.html',
859         post_delete_redirect=reverse(
860             'group_detail',
861             kwargs=dict(group_id=group_id)
862         )
863     )
864
865
866 def handle_membership(func):
867     @wraps(func)
868     def wrapper(request, group_id, user_id):
869         try:
870             m = Membership.objects.select_related().get(
871                 group__id=group_id,
872                 person__id=user_id
873             )
874         except Membership.DoesNotExist:
875             return HttpResponseBadRequest(_('Invalid membership.'))
876         else:
877             if request.user not in m.group.owner.all():
878                 return HttpResponseForbidden(_('User is not a group owner.'))
879             func(request, m)
880             return render_response(
881                 template='im/astakosgroup_detail.html',
882                 context_instance=get_context(request),
883                 object=m.group,
884                 quota=m.group.quota
885             )
886     return wrapper
887
888
889 @signed_terms_required
890 @login_required
891 @handle_membership
892 def approve_member(request, membership):
893     try:
894         membership.approve()
895         realname = membership.person.realname
896         msg = _('%s has been successfully joined the group.' % realname)
897         messages.success(request, msg)
898     except BaseException, e:
899         logger.exception(e)
900         msg = _('Something went wrong during %s\'s approval.' % realname)
901         messages.error(request, msg)
902
903
904 @signed_terms_required
905 @login_required
906 @handle_membership
907 def disapprove_member(request, membership):
908     try:
909         membership.disapprove()
910         realname = membership.person.realname
911         msg = _('%s has been successfully removed from the group.' % realname)
912         messages.success(request, msg)
913     except BaseException, e:
914         logger.exception(e)
915         msg = _('Something went wrong during %s\'s disapproval.' % realname)
916         messages.error(request, msg)
917
918
919
920
921 @signed_terms_required
922 @login_required
923 def add_members(request, group_id):
924     if request.method != 'POST':
925         return HttpResponseBadRequest(_('Bad method'))
926     try:
927         group = AstakosGroup.objects.select_related().get(id=group_id)
928     except AstakosGroup.DoesNotExist:
929         return HttpResponseBadRequest(_('Invalid group.'))
930     search_form = AddGroupMembersForm(request.POST)
931     if search_form.is_valid():
932         users = search_form.get_valid_users()
933         map(group.approve_member, users)
934         search_form = AddGroupMembersForm()
935     form = AstakosGroupUpdateForm(instance=group)
936     return object_detail(request,
937                          AstakosGroup.objects.all(),
938                          object_id=group_id,
939                          extra_context={'quota': group.quota,
940                                         'form': form,
941                                         'search_form' : search_form}
942                          )
943
944
945 @signed_terms_required
946 @login_required
947 def resource_list(request):
948     return render_response(
949         template='im/astakosuserquota_list.html',
950         context_instance=get_context(request),
951         quota=request.user.quota
952     )
953
954
955 def group_create_list(request):
956     return render_response(
957         template='im/astakosgroup_create_list.html',
958         context_instance=get_context(request),
959     )
960
961
962 @signed_terms_required
963 @login_required
964 def billing(request):
965     
966     today = datetime.today()
967     month_last_day= calendar.monthrange(today.year, today.month)[1]
968     
969     start = request.POST.get('datefrom', None)
970     if start:
971         today = datetime.fromtimestamp(int(start))
972         month_last_day= calendar.monthrange(today.year, today.month)[1]
973     
974     start = datetime(today.year, today.month, 1).strftime("%s")
975     end = datetime(today.year, today.month, month_last_day).strftime("%s")
976     r = request_billing.apply(args=('pgerakios@grnet.gr',
977                                     int(start) * 1000,
978                                     int(end) * 1000))
979     data = {}
980     
981     try:
982         status, data = r.result
983         data=clear_billing_data(data)
984         if status != 200:
985             messages.error(request, _('Service response status: %d' % status))
986     except:
987         messages.error(request, r.result)
988     
989     print type(start)
990     
991     return render_response(
992         template='im/billing.html',
993         context_instance=get_context(request),
994         data=data,
995         zerodate=datetime(month=1,year=1970, day=1),
996         today=today,
997         start=int(start),
998         month_last_day=month_last_day)  
999     
1000 def clear_billing_data(data):
1001     
1002     # remove addcredits entries
1003     def isnotcredit(e):
1004         return e['serviceName'] != "addcredits"
1005     
1006     
1007     
1008     # separate services    
1009     def servicefilter(service_name):
1010         service = service_name
1011         def fltr(e):
1012             return e['serviceName'] == service
1013         return fltr
1014         
1015     
1016     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1017     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1018     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1019     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1020         
1021     return data