New timeline tab
[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.settings import (
79     COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
80     LOGGING_LEVEL, PAGINATE_BY)
81 from astakos.im.tasks import request_billing
82
83 logger = logging.getLogger(__name__)
84
85
86 def render_response(template, tab=None, status=200, reset_cookie=False,
87                     context_instance=None, **kwargs):
88     """
89     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
90     keyword argument and returns an ``django.http.HttpResponse`` with the
91     specified ``status``.
92     """
93     if tab is None:
94         tab = template.partition('_')[0].partition('.html')[0]
95     kwargs.setdefault('tab', tab)
96     html = loader.render_to_string(
97         template, kwargs, context_instance=context_instance)
98     response = HttpResponse(html, status=status)
99     if reset_cookie:
100         set_cookie(response, context_instance['request'].user)
101     return response
102
103
104 def requires_anonymous(func):
105     """
106     Decorator checkes whether the request.user is not Anonymous and in that case
107     redirects to `logout`.
108     """
109     @wraps(func)
110     def wrapper(request, *args):
111         if not request.user.is_anonymous():
112             next = urlencode({'next': request.build_absolute_uri()})
113             logout_uri = reverse(logout) + '?' + next
114             return HttpResponseRedirect(logout_uri)
115         return func(request, *args)
116     return wrapper
117
118
119 def signed_terms_required(func):
120     """
121     Decorator checkes whether the request.user is Anonymous and in that case
122     redirects to `logout`.
123     """
124     @wraps(func)
125     def wrapper(request, *args, **kwargs):
126         if request.user.is_authenticated() and not request.user.signed_terms:
127             params = urlencode({'next': request.build_absolute_uri(),
128                                 'show_form': ''})
129             terms_uri = reverse('latest_terms') + '?' + params
130             return HttpResponseRedirect(terms_uri)
131         return func(request, *args, **kwargs)
132     return wrapper
133
134
135 @signed_terms_required
136 def index(request, login_template_name='im/login.html', extra_context=None):
137     """
138     If there is logged on user renders the profile page otherwise renders login page.
139
140     **Arguments**
141
142     ``login_template_name``
143         A custom login template to use. This is optional; if not specified,
144         this will default to ``im/login.html``.
145
146     ``profile_template_name``
147         A custom profile template to use. This is optional; if not specified,
148         this will default to ``im/profile.html``.
149
150     ``extra_context``
151         An dictionary of variables to add to the template context.
152
153     **Template:**
154
155     im/profile.html or im/login.html or ``template_name`` keyword argument.
156
157     """
158     template_name = login_template_name
159     if request.user.is_authenticated():
160         return HttpResponseRedirect(reverse('edit_profile'))
161     return render_response(template_name,
162                            login_form=LoginForm(request=request),
163                            context_instance=get_context(request, extra_context))
164
165
166 @login_required
167 @signed_terms_required
168 @transaction.commit_manually
169 def invite(request, template_name='im/invitations.html', extra_context=None):
170     """
171     Allows a user to invite somebody else.
172
173     In case of GET request renders a form for providing the invitee information.
174     In case of POST checks whether the user has not run out of invitations and then
175     sends an invitation email to singup to the service.
176
177     The view uses commit_manually decorator in order to ensure the number of the
178     user invitations is going to be updated only if the email has been successfully sent.
179
180     If the user isn't logged in, redirects to settings.LOGIN_URL.
181
182     **Arguments**
183
184     ``template_name``
185         A custom template to use. This is optional; if not specified,
186         this will default to ``im/invitations.html``.
187
188     ``extra_context``
189         An dictionary of variables to add to the template context.
190
191     **Template:**
192
193     im/invitations.html or ``template_name`` keyword argument.
194
195     **Settings:**
196
197     The view expectes the following settings are defined:
198
199     * LOGIN_URL: login uri
200     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
201     """
202     status = None
203     message = None
204     form = InvitationForm()
205
206     inviter = request.user
207     if request.method == 'POST':
208         form = InvitationForm(request.POST)
209         if inviter.invitations > 0:
210             if form.is_valid():
211                 try:
212                     invitation = form.save()
213                     invite_func(invitation, inviter)
214                     message = _('Invitation sent to %s' % invitation.username)
215                     messages.success(request, message)
216                 except SendMailError, e:
217                     message = e.message
218                     messages.error(request, message)
219                     transaction.rollback()
220                 except BaseException, e:
221                     message = _('Something went wrong.')
222                     messages.error(request, message)
223                     logger.exception(e)
224                     transaction.rollback()
225                 else:
226                     transaction.commit()
227         else:
228             message = _('No invitations left')
229             messages.error(request, message)
230
231     sent = [{'email': inv.username,
232              'realname': inv.realname,
233              'is_consumed': inv.is_consumed}
234             for inv in request.user.invitations_sent.all()]
235     kwargs = {'inviter': inviter,
236               'sent': sent}
237     context = get_context(request, extra_context, **kwargs)
238     return render_response(template_name,
239                            invitation_form=form,
240                            context_instance=context)
241
242
243 @login_required
244 @signed_terms_required
245 def edit_profile(request, template_name='im/profile.html', extra_context=None):
246     """
247     Allows a user to edit his/her profile.
248
249     In case of GET request renders a form for displaying the user information.
250     In case of POST updates the user informantion and redirects to ``next``
251     url parameter if exists.
252
253     If the user isn't logged in, redirects to settings.LOGIN_URL.
254
255     **Arguments**
256
257     ``template_name``
258         A custom template to use. This is optional; if not specified,
259         this will default to ``im/profile.html``.
260
261     ``extra_context``
262         An dictionary of variables to add to the template context.
263
264     **Template:**
265
266     im/profile.html or ``template_name`` keyword argument.
267
268     **Settings:**
269
270     The view expectes the following settings are defined:
271
272     * LOGIN_URL: login uri
273     """
274     extra_context = extra_context or {}
275     form = ProfileForm(instance=request.user)
276     extra_context['next'] = request.GET.get('next')
277     reset_cookie = False
278     if request.method == 'POST':
279         form = ProfileForm(request.POST, instance=request.user)
280         if form.is_valid():
281             try:
282                 prev_token = request.user.auth_token
283                 user = form.save()
284                 reset_cookie = user.auth_token != prev_token
285                 form = ProfileForm(instance=user)
286                 next = request.POST.get('next')
287                 if next:
288                     return redirect(next)
289                 msg = _('Profile has been updated successfully')
290                 messages.success(request, msg)
291             except ValueError, ve:
292                 messages.success(request, ve)
293     elif request.method == "GET":
294         if not request.user.is_verified:
295             request.user.is_verified = True
296             request.user.save()
297     return render_response(template_name,
298                            reset_cookie=reset_cookie,
299                            profile_form=form,
300                            context_instance=get_context(request,
301                                                         extra_context))
302
303
304 @transaction.commit_manually
305 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
306     """
307     Allows a user to create a local account.
308
309     In case of GET request renders a form for entering the user information.
310     In case of POST handles the signup.
311
312     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
313     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
314     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
315     (see activation_backends);
316
317     Upon successful user creation, if ``next`` url parameter is present the user is redirected there
318     otherwise renders the same page with a success message.
319
320     On unsuccessful creation, renders ``template_name`` with an error message.
321
322     **Arguments**
323
324     ``template_name``
325         A custom template to render. This is optional;
326         if not specified, this will default to ``im/signup.html``.
327
328     ``on_success``
329         A custom template to render in case of success. This is optional;
330         if not specified, this will default to ``im/signup_complete.html``.
331
332     ``extra_context``
333         An dictionary of variables to add to the template context.
334
335     **Template:**
336
337     im/signup.html or ``template_name`` keyword argument.
338     im/signup_complete.html or ``on_success`` keyword argument.
339     """
340     if request.user.is_authenticated():
341         return HttpResponseRedirect(reverse('edit_profile'))
342
343     provider = get_query(request).get('provider', 'local')
344     try:
345         if not backend:
346             backend = get_backend(request)
347         form = backend.get_signup_form(provider)
348     except Exception, e:
349         form = SimpleBackend(request).get_signup_form(provider)
350         messages.error(request, e)
351     if request.method == 'POST':
352         if form.is_valid():
353             user = form.save(commit=False)
354             try:
355                 result = backend.handle_activation(user)
356                 status = messages.SUCCESS
357                 message = result.message
358                 user.save()
359                 if 'additional_email' in form.cleaned_data:
360                     additional_email = form.cleaned_data['additional_email']
361                     if additional_email != user.email:
362                         user.additionalmail_set.create(email=additional_email)
363                         msg = 'Additional email: %s saved for user %s.' % (
364                             additional_email, user.email)
365                         logger.log(LOGGING_LEVEL, msg)
366                 if user and user.is_active:
367                     next = request.POST.get('next', '')
368                     transaction.commit()
369                     return prepare_response(request, user, next=next)
370                 messages.add_message(request, status, message)
371                 transaction.commit()
372                 return render_response(on_success,
373                                        context_instance=get_context(request, extra_context))
374             except SendMailError, e:
375                 message = e.message
376                 messages.error(request, message)
377                 transaction.rollback()
378             except BaseException, e:
379                 message = _('Something went wrong.')
380                 messages.error(request, message)
381                 logger.exception(e)
382                 transaction.rollback()
383     return render_response(template_name,
384                            signup_form=form,
385                            provider=provider,
386                            context_instance=get_context(request, extra_context))
387
388
389 @login_required
390 @signed_terms_required
391 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
392     """
393     Allows a user to send feedback.
394
395     In case of GET request renders a form for providing the feedback information.
396     In case of POST sends an email to support team.
397
398     If the user isn't logged in, redirects to settings.LOGIN_URL.
399
400     **Arguments**
401
402     ``template_name``
403         A custom template to use. This is optional; if not specified,
404         this will default to ``im/feedback.html``.
405
406     ``extra_context``
407         An dictionary of variables to add to the template context.
408
409     **Template:**
410
411     im/signup.html or ``template_name`` keyword argument.
412
413     **Settings:**
414
415     * LOGIN_URL: login uri
416     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
417     """
418     if request.method == 'GET':
419         form = FeedbackForm()
420     if request.method == 'POST':
421         if not request.user:
422             return HttpResponse('Unauthorized', status=401)
423
424         form = FeedbackForm(request.POST)
425         if form.is_valid():
426             msg = form.cleaned_data['feedback_msg']
427             data = form.cleaned_data['feedback_data']
428             try:
429                 send_feedback(msg, data, request.user, email_template_name)
430             except SendMailError, e:
431                 messages.error(request, message)
432             else:
433                 message = _('Feedback successfully sent')
434                 messages.success(request, message)
435     return render_response(template_name,
436                            feedback_form=form,
437                            context_instance=get_context(request, extra_context))
438
439
440 @signed_terms_required
441 def logout(request, template='registration/logged_out.html', extra_context=None):
442     """
443     Wraps `django.contrib.auth.logout` and delete the cookie.
444     """
445     response = HttpResponse()
446     if request.user.is_authenticated():
447         email = request.user.email
448         auth_logout(request)
449         response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
450         msg = 'Cookie deleted for %s' % email
451         logger.log(LOGGING_LEVEL, msg)
452     next = request.GET.get('next')
453     if next:
454         response['Location'] = next
455         response.status_code = 302
456         return response
457     elif LOGOUT_NEXT:
458         response['Location'] = LOGOUT_NEXT
459         response.status_code = 301
460         return response
461     messages.success(request, _('You have successfully logged out.'))
462     context = get_context(request, extra_context)
463     response.write(loader.render_to_string(template, context_instance=context))
464     return response
465
466
467 @transaction.commit_manually
468 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
469     """
470     Activates the user identified by the ``auth`` request parameter, sends a welcome email
471     and renews the user token.
472
473     The view uses commit_manually decorator in order to ensure the user state will be updated
474     only if the email will be send successfully.
475     """
476     token = request.GET.get('auth')
477     next = request.GET.get('next')
478     try:
479         user = AstakosUser.objects.get(auth_token=token)
480     except AstakosUser.DoesNotExist:
481         return HttpResponseBadRequest(_('No such user'))
482
483     if user.is_active:
484         message = _('Account already active.')
485         messages.error(request, message)
486         return index(request)
487
488     try:
489         local_user = AstakosUser.objects.get(
490             ~Q(id=user.id),
491             email=user.email,
492             is_active=True
493         )
494     except AstakosUser.DoesNotExist:
495         try:
496             activate_func(
497                 user,
498                 greeting_email_template_name,
499                 helpdesk_email_template_name,
500                 verify_email=True
501             )
502             response = prepare_response(request, user, next, renew=True)
503             transaction.commit()
504             return response
505         except SendMailError, e:
506             message = e.message
507             messages.error(request, message)
508             transaction.rollback()
509             return index(request)
510         except BaseException, e:
511             message = _('Something went wrong.')
512             messages.error(request, message)
513             logger.exception(e)
514             transaction.rollback()
515             return index(request)
516     else:
517         try:
518             user = switch_account_to_shibboleth(
519                 user,
520                 local_user,
521                 greeting_email_template_name
522             )
523             response = prepare_response(request, user, next, renew=True)
524             transaction.commit()
525             return response
526         except SendMailError, e:
527             message = e.message
528             messages.error(request, message)
529             transaction.rollback()
530             return index(request)
531         except BaseException, e:
532             message = _('Something went wrong.')
533             messages.error(request, message)
534             logger.exception(e)
535             transaction.rollback()
536             return index(request)
537
538
539 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
540     term = None
541     terms = None
542     if not term_id:
543         try:
544             term = ApprovalTerms.objects.order_by('-id')[0]
545         except IndexError:
546             pass
547     else:
548         try:
549             term = ApprovalTerms.objects.get(id=term_id)
550         except ApprovalTerms.DoesNotExist, e:
551             pass
552
553     if not term:
554         return HttpResponseRedirect(reverse('index'))
555     f = open(term.location, 'r')
556     terms = f.read()
557
558     if request.method == 'POST':
559         next = request.POST.get('next')
560         if not next:
561             next = reverse('index')
562         form = SignApprovalTermsForm(request.POST, instance=request.user)
563         if not form.is_valid():
564             return render_response(template_name,
565                                    terms=terms,
566                                    approval_terms_form=form,
567                                    context_instance=get_context(request, extra_context))
568         user = form.save()
569         return HttpResponseRedirect(next)
570     else:
571         form = None
572         if request.user.is_authenticated() and not request.user.signed_terms:
573             form = SignApprovalTermsForm(instance=request.user)
574         return render_response(template_name,
575                                terms=terms,
576                                approval_terms_form=form,
577                                context_instance=get_context(request, extra_context))
578
579
580 @signed_terms_required
581 def change_password(request):
582     return password_change(request,
583                            post_change_redirect=reverse('edit_profile'),
584                            password_change_form=ExtendedPasswordChangeForm)
585
586
587 @signed_terms_required
588 @login_required
589 @transaction.commit_manually
590 def change_email(request, activation_key=None,
591                  email_template_name='registration/email_change_email.txt',
592                  form_template_name='registration/email_change_form.html',
593                  confirm_template_name='registration/email_change_done.html',
594                  extra_context=None):
595     if activation_key:
596         try:
597             user = EmailChange.objects.change_email(activation_key)
598             if request.user.is_authenticated() and request.user == user:
599                 msg = _('Email changed successfully.')
600                 messages.success(request, msg)
601                 auth_logout(request)
602                 response = prepare_response(request, user)
603                 transaction.commit()
604                 return response
605         except ValueError, e:
606             messages.error(request, e)
607         return render_response(confirm_template_name,
608                                modified_user=user if 'user' in locals(
609                                ) else None,
610                                context_instance=get_context(request,
611                                                             extra_context))
612
613     if not request.user.is_authenticated():
614         path = quote(request.get_full_path())
615         url = request.build_absolute_uri(reverse('index'))
616         return HttpResponseRedirect(url + '?next=' + path)
617     form = EmailChangeForm(request.POST or None)
618     if request.method == 'POST' and form.is_valid():
619         try:
620             ec = form.save(email_template_name, request)
621         except SendMailError, e:
622             msg = e
623             messages.error(request, msg)
624             transaction.rollback()
625         except IntegrityError, e:
626             msg = _('There is already a pending change email request.')
627             messages.error(request, msg)
628         else:
629             msg = _('Change email request has been registered succefully.\
630                     You are going to receive a verification email in the new address.')
631             messages.success(request, msg)
632             transaction.commit()
633     return render_response(form_template_name,
634                            form=form,
635                            context_instance=get_context(request,
636                                                         extra_context))
637
638
639 @signed_terms_required
640 @login_required
641 def group_add(request, kind_name='default'):
642     try:
643         kind = GroupKind.objects.get(name=kind_name)
644     except:
645         return HttpResponseBadRequest(_('No such group kind'))
646
647     template_loader = loader
648     post_save_redirect = '/im/group/%(id)s/'
649     context_processors = None
650     model, form_class = get_model_and_form_class(
651         model=None,
652         form_class=AstakosGroupCreationForm
653     )
654     resources = dict(
655         (str(r.id), r) for r in Resource.objects.select_related().all())
656     policies = []
657     if request.method == 'POST':
658         form = form_class(request.POST, request.FILES, resources=resources)
659         if form.is_valid():
660             new_object = form.save()
661
662             # save owner
663             new_object.owners = [request.user]
664
665             # save quota policies
666             for (rid, uplimit) in form.resources():
667                 try:
668                     r = resources[rid]
669                 except KeyError, e:
670                     logger.exception(e)
671                     # TODO Should I stay or should I go???
672                     continue
673                 else:
674                     new_object.astakosgroupquota_set.create(
675                         resource=r,
676                         uplimit=uplimit
677                     )
678                 policies.append('%s %d' % (r, uplimit))
679             msg = _("The %(verbose_name)s was created successfully.") %\
680                 {"verbose_name": model._meta.verbose_name}
681             messages.success(request, msg, fail_silently=True)
682
683             # send notification
684             try:
685                 send_admin_notification(
686                     template_name='im/group_creation_notification.txt',
687                     dictionary={
688                         'group': new_object,
689                         'owner': request.user,
690                         'policies': policies,
691                     },
692                     subject='%s alpha2 testing group creation notification' % SITENAME
693                 )
694             except SendNotificationError, e:
695                 messages.error(request, e, fail_silently=True)
696             return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
697     else:
698         now = datetime.now()
699         data = {
700             'kind': kind
701         }
702         form = form_class(data, resources=resources)
703
704     # Create the template, context, response
705     template_name = "%s/%s_form.html" % (
706         model._meta.app_label,
707         model._meta.object_name.lower()
708     )
709     t = template_loader.get_template(template_name)
710     c = RequestContext(request, {
711         'form': form,
712         'kind': kind,
713     }, context_processors)
714     return HttpResponse(t.render(c))
715
716
717 @signed_terms_required
718 @login_required
719 def group_list(request):
720     q = request.user.astakos_groups.none()
721     list = request.user.astakos_groups.select_related().all()
722     d = {}
723     d['own'] = [g for g in list if request.user in g.owner.all()]
724     d['other'] = list.exclude(id__in=(g.id for g in d['own']))
725     for k, queryset in d.iteritems():
726         paginator = Paginator(queryset, PAGINATE_BY)
727         page = request.GET.get('%s_page' % k, 1)
728         try:
729             page_number = int(page)
730         except ValueError:
731             if page == 'last':
732                 page_number = paginator.num_pages
733             else:
734                 # Page is not 'last', nor can it be converted to an int.
735                 raise Http404
736         try:
737             page_obj = locals()['%s_page_obj' % k] = paginator.page(page_number)
738         except InvalidPage:
739             raise Http404
740     return object_list(request, queryset=q,
741                        extra_context={'is_search':False,
742                                       'mine': locals()['own_page_obj'],
743                                       'other': locals()['other_page_obj']})
744
745
746 @signed_terms_required
747 @login_required
748 def group_detail(request, group_id):
749     try:
750         group = AstakosGroup.objects.select_related().get(id=group_id)
751     except AstakosGroup.DoesNotExist:
752         return HttpResponseBadRequest(_('Invalid group.'))
753     form = AstakosGroupUpdateForm(instance=group)
754     search_form = AddGroupMembersForm()
755     return object_detail(request,
756                          AstakosGroup.objects.all(),
757                          object_id=group_id,
758                          extra_context={'quota': group.quota,
759                                         'form': form,
760                                         'search_form': search_form}
761                          )
762
763
764 @signed_terms_required
765 @login_required
766 def group_update(request, group_id):
767     if request.method != 'POST':
768         return HttpResponseBadRequest('Method not allowed.')
769     try:
770         group = AstakosGroup.objects.select_related().get(id=group_id)
771     except AstakosGroup.DoesNotExist:
772         return HttpResponseBadRequest(_('Invalid group.'))
773     form = AstakosGroupUpdateForm(request.POST, instance=group)
774     if form.is_valid():
775         form.save()
776     search_form = AddGroupMembersForm()
777     return object_detail(request,
778                          AstakosGroup.objects.all(),
779                          object_id=group_id,
780                          extra_context={'quota': group.quota,
781                                         'form': form,
782                                         'search_form': search_form})
783
784 @signed_terms_required
785 @login_required
786 def group_search(request, extra_context=None, **kwargs):
787     q = request.GET.get('q')
788     if request.method == 'GET':
789         form = AstakosGroupSearchForm({'q': q} if q else None)
790     else:
791         form = AstakosGroupSearchForm(get_query(request))
792         if form.is_valid():
793             q = form.cleaned_data['q'].strip()
794     if q:
795         queryset = AstakosGroup.objects.select_related(
796         ).filter(name__contains=q)
797     else:
798         queryset = AstakosGroup.objects.none()
799     return object_list(
800         request,
801         queryset,
802         paginate_by=PAGINATE_BY,
803         page=request.GET.get('page') or 1,
804         template_name='im/astakosgroup_list.html',
805         extra_context=dict(form=form,
806                            is_search=True,
807                            q=q))
808
809 @signed_terms_required
810 @login_required
811 def group_all(request, extra_context=None, **kwargs):
812     return object_list(
813                 request,
814                 AstakosGroup.objects.select_related().all(),
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    
1023
1024 def timeline(request):
1025     data = None
1026     l = []
1027     if request.method == 'POST':
1028         data = request.POST
1029         l =[(u'test/apples', u'2012-09-20T11:39:53.1797', 192172055040L, 25569215632749L),
1030             (u'test/pears/kate.jpg', u'2012-09-20T11:45:14.1153', 381640896L, 25626036449581L),
1031             (u'test/pears/kate_beckinsale.jpg', u'2012-09-20T11:45:14.5830', 0L, 25626036449581L),
1032             (u'test/pears/How To Win Friends And Influence People.pdf', u'2012-09-20T11:45:15.0694', 0L, 25626036449581L),
1033             (u'test/pears/moms_birthday.jpg', u'2012-09-20T11:45:15.5615', 0L, 25626036449581L),
1034             (u'test/pears/poodle_strut.mov', u'2012-09-20T11:45:16.0290', 0L, 25626036449581L),
1035             (u'test/pears/Disturbed - Down With The Sickness.mp3', u'2012-09-20T11:45:16.4950', 0L, 25626036449581L),
1036             (u'test/pears/army_of_darkness.avi', u'2012-09-20T11:45:16.9844', 0L, 25626036449581L),
1037             (u'test/pears/the_mad.avi', u'2012-09-20T11:45:17.4516', 0L, 25626036449581L),
1038             (u'test/apples/photos/animals/dogs/poodle.jpg', u'2012-09-20T11:45:17.9281', 0L, 25626036449581L),
1039             (u'test/apples/photos/animals/dogs/terrier.jpg', u'2012-09-20T11:45:18.3918', 0L, 25626036449581L),
1040             (u'test/apples/photos/animals/cats/persian.jpg', u'2012-09-20T11:45:18.8626', 0L, 25626036449581L),
1041             (u'test/apples/photos/animals/cats/siamese.jpg', u'2012-09-20T11:45:19.3686', 0L, 25626036449581L),
1042             (u'test/apples/photos/plants/fern.jpg', u'2012-09-20T11:45:19.8464', 0L, 25626036449581L),
1043             (u'test/apples/photos/plants/rose.jpg', u'2012-09-20T11:45:20.7334', 0L, 25626036449581L)]
1044     
1045     form = TimelineForm(data)
1046     return render_response(template='im/timeline.html',
1047                            context_instance=get_context(request),
1048                            form=form,
1049                            l=l)