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