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     
756     for k, l in d.iteritems():
757         page = request.GET.get('%s_page' % k, 1)
758         sorting = globals()['%s_sorting' % k] = request.GET.get('%s_sorting' % k)
759         if sorting:
760             sort_form = AstakosGroupSortForm({'sort_by': sorting})
761             if sort_form.is_valid():
762                 l.sort(key=lambda i: getattr(i, sorting))
763                 globals()['%s_sorting' % k] = sorting
764         paginator = Paginator(l, PAGINATE_BY)
765         
766         try:
767             page_number = int(page)
768         except ValueError:
769             if page == 'last':
770                 page_number = paginator.num_pages
771             else:
772                 # Page is not 'last', nor can it be converted to an int.
773                 raise Http404
774         try:
775             page_obj = globals()['%s_page_obj' % k] = paginator.page(page_number)
776         except InvalidPage:
777             raise Http404
778     return object_list(request, queryset=none,
779                        extra_context={'is_search':False,
780                                       'mine': own_page_obj,
781                                       'other': other_page_obj,
782                                       'own_sorting': own_sorting,
783                                       'other_sorting': other_sorting
784                                       })
785
786
787 @signed_terms_required
788 @login_required
789 def group_detail(request, group_id):
790     try:
791         group = AstakosGroup.objects.select_related().get(id=group_id)
792     except AstakosGroup.DoesNotExist:
793         return HttpResponseBadRequest(_('Invalid group.'))
794     form = AstakosGroupUpdateForm(instance=group)
795     search_form = AddGroupMembersForm()
796     return object_detail(request,
797                          AstakosGroup.objects.all(),
798                          object_id=group_id,
799                          extra_context={'quota': group.quota,
800                                         'form': form,
801                                         'search_form': search_form}
802                          )
803
804
805 @signed_terms_required
806 @login_required
807 def group_update(request, group_id):
808     if request.method != 'POST':
809         return HttpResponseBadRequest('Method not allowed.')
810     try:
811         group = AstakosGroup.objects.select_related().get(id=group_id)
812     except AstakosGroup.DoesNotExist:
813         return HttpResponseBadRequest(_('Invalid group.'))
814     form = AstakosGroupUpdateForm(request.POST, instance=group)
815     if form.is_valid():
816         form.save()
817     search_form = AddGroupMembersForm()
818     return object_detail(request,
819                          AstakosGroup.objects.all(),
820                          object_id=group_id,
821                          extra_context={'quota': group.quota,
822                                         'form': form,
823                                         'search_form': search_form})
824
825 @signed_terms_required
826 @login_required
827 def group_search(request, extra_context=None, **kwargs):
828     q = request.GET.get('q')
829     if request.method == 'GET':
830         form = AstakosGroupSearchForm({'q': q} if q else None)
831     else:
832         form = AstakosGroupSearchForm(get_query(request))
833         if form.is_valid():
834             q = form.cleaned_data['q'].strip()
835     if q:
836         queryset = AstakosGroup.objects.select_related()
837         queryset = queryset.filter(name__contains=q)
838         queryset = queryset.filter(approval_date__isnull=False)
839         queryset = queryset.extra(select={
840                 'groupname': DB_REPLACE_GROUP_SCHEME,
841                 'kindname': "im_groupkind.name",
842                 'approved_members_num': """
843                     SELECT COUNT(*) FROM im_membership
844                     WHERE group_id = im_astakosgroup.group_ptr_id
845                     AND date_joined IS NOT NULL""",
846                 'membership_approval_date': """
847                     SELECT date_joined FROM im_membership
848                     WHERE group_id = im_astakosgroup.group_ptr_id
849                     AND person_id = %s""" % request.user.id,
850                 'is_member': """
851                     SELECT CASE WHEN EXISTS(
852                     SELECT date_joined FROM im_membership
853                     WHERE group_id = im_astakosgroup.group_ptr_id
854                     AND person_id = %s)
855                     THEN 1 ELSE 0 END""" % request.user.id})
856     else:
857         queryset = AstakosGroup.objects.none()
858     return object_list(
859         request,
860         queryset,
861         paginate_by=PAGINATE_BY,
862         page=request.GET.get('page') or 1,
863         template_name='im/astakosgroup_list.html',
864         extra_context=dict(form=form,
865                            is_search=True,
866                            q=q))
867
868 @signed_terms_required
869 @login_required
870 def group_all(request, extra_context=None, **kwargs):
871     q = AstakosGroup.objects.select_related()
872     q = q.filter(approval_date__isnull=False)
873     q = q.extra(select={
874                 'groupname': DB_REPLACE_GROUP_SCHEME,
875                 'kindname': "im_groupkind.name",
876                 'approved_members_num': """
877                     SELECT COUNT(*) FROM im_membership
878                     WHERE group_id = im_astakosgroup.group_ptr_id
879                     AND date_joined IS NOT NULL""",
880                 'membership_approval_date': """
881                     SELECT date_joined FROM im_membership
882                     WHERE group_id = im_astakosgroup.group_ptr_id
883                     AND person_id = %s""" % request.user.id,
884                 'is_member': """
885                     SELECT CASE WHEN EXISTS(
886                     SELECT date_joined FROM im_membership
887                     WHERE group_id = im_astakosgroup.group_ptr_id
888                     AND person_id = %s)
889                     THEN 1 ELSE 0 END""" % request.user.id})
890     return object_list(
891                 request,
892                 q,
893                 paginate_by=PAGINATE_BY,
894                 page=request.GET.get('page') or 1,
895                 template_name='im/astakosgroup_list.html',
896                 extra_context=dict(form=AstakosGroupSearchForm(),
897                                    is_search=True))
898
899
900 @signed_terms_required
901 @login_required
902 def group_join(request, group_id):
903     m = Membership(group_id=group_id,
904                    person=request.user,
905                    date_requested=datetime.now())
906     try:
907         m.save()
908         post_save_redirect = reverse(
909             'group_detail',
910             kwargs=dict(group_id=group_id))
911         return HttpResponseRedirect(post_save_redirect)
912     except IntegrityError, e:
913         logger.exception(e)
914         msg = _('Failed to join group.')
915         messages.error(request, msg)
916         return group_search(request)
917
918
919 @signed_terms_required
920 @login_required
921 def group_leave(request, group_id):
922     try:
923         m = Membership.objects.select_related().get(
924             group__id=group_id,
925             person=request.user
926         )
927     except Membership.DoesNotExist:
928         return HttpResponseBadRequest(_('Invalid membership.'))
929     if request.user in m.group.owner.all():
930         return HttpResponseForbidden(_('Owner can not leave the group.'))
931     return delete_object(
932         request,
933         model=Membership,
934         object_id=m.id,
935         template_name='im/astakosgroup_list.html',
936         post_delete_redirect=reverse(
937             'group_detail',
938             kwargs=dict(group_id=group_id)
939         )
940     )
941
942
943 def handle_membership(func):
944     @wraps(func)
945     def wrapper(request, group_id, user_id):
946         try:
947             m = Membership.objects.select_related().get(
948                 group__id=group_id,
949                 person__id=user_id
950             )
951         except Membership.DoesNotExist:
952             return HttpResponseBadRequest(_('Invalid membership.'))
953         else:
954             if request.user not in m.group.owner.all():
955                 return HttpResponseForbidden(_('User is not a group owner.'))
956             func(request, m)
957             return render_response(
958                 template='im/astakosgroup_detail.html',
959                 context_instance=get_context(request),
960                 object=m.group,
961                 quota=m.group.quota
962             )
963     return wrapper
964
965
966 @signed_terms_required
967 @login_required
968 @handle_membership
969 def approve_member(request, membership):
970     try:
971         membership.approve()
972         realname = membership.person.realname
973         msg = _('%s has been successfully joined the group.' % realname)
974         messages.success(request, msg)
975     except BaseException, e:
976         logger.exception(e)
977         msg = _('Something went wrong during %s\'s approval.' % realname)
978         messages.error(request, msg)
979
980
981 @signed_terms_required
982 @login_required
983 @handle_membership
984 def disapprove_member(request, membership):
985     try:
986         membership.disapprove()
987         realname = membership.person.realname
988         msg = _('%s has been successfully removed from the group.' % realname)
989         messages.success(request, msg)
990     except BaseException, e:
991         logger.exception(e)
992         msg = _('Something went wrong during %s\'s disapproval.' % realname)
993         messages.error(request, msg)
994
995
996
997
998 @signed_terms_required
999 @login_required
1000 def add_members(request, group_id):
1001     if request.method != 'POST':
1002         return HttpResponseBadRequest(_('Bad method'))
1003     try:
1004         group = AstakosGroup.objects.select_related().get(id=group_id)
1005     except AstakosGroup.DoesNotExist:
1006         return HttpResponseBadRequest(_('Invalid group.'))
1007     search_form = AddGroupMembersForm(request.POST)
1008     if search_form.is_valid():
1009         users = search_form.get_valid_users()
1010         map(group.approve_member, users)
1011         search_form = AddGroupMembersForm()
1012     form = AstakosGroupUpdateForm(instance=group)
1013     return object_detail(request,
1014                          AstakosGroup.objects.all(),
1015                          object_id=group_id,
1016                          extra_context={'quota': group.quota,
1017                                         'form': form,
1018                                         'search_form' : search_form})
1019
1020
1021 @signed_terms_required
1022 @login_required
1023 def resource_list(request):
1024     return render_response(
1025         template='im/astakosuserquota_list.html',
1026         context_instance=get_context(request),
1027         quota=request.user.quota
1028     )
1029
1030
1031 def group_create_list(request):
1032     return render_response(
1033         template='im/astakosgroup_create_list.html',
1034         context_instance=get_context(request),
1035     )
1036
1037
1038 @signed_terms_required
1039 @login_required
1040 def billing(request):
1041     
1042     today = datetime.today()
1043     month_last_day= calendar.monthrange(today.year, today.month)[1]
1044     
1045     start = request.POST.get('datefrom', None)
1046     if start:
1047         today = datetime.fromtimestamp(int(start))
1048         month_last_day= calendar.monthrange(today.year, today.month)[1]
1049     
1050     start = datetime(today.year, today.month, 1).strftime("%s")
1051     end = datetime(today.year, today.month, month_last_day).strftime("%s")
1052     r = request_billing.apply(args=('pgerakios@grnet.gr',
1053                                     int(start) * 1000,
1054                                     int(end) * 1000))
1055     data = {}
1056     
1057     try:
1058         status, data = r.result
1059         data=clear_billing_data(data)
1060         if status != 200:
1061             messages.error(request, _('Service response status: %d' % status))
1062     except:
1063         messages.error(request, r.result)
1064     
1065     print type(start)
1066     
1067     return render_response(
1068         template='im/billing.html',
1069         context_instance=get_context(request),
1070         data=data,
1071         zerodate=datetime(month=1,year=1970, day=1),
1072         today=today,
1073         start=int(start),
1074         month_last_day=month_last_day)  
1075     
1076 def clear_billing_data(data):
1077     
1078     # remove addcredits entries
1079     def isnotcredit(e):
1080         return e['serviceName'] != "addcredits"
1081     
1082     
1083     
1084     # separate services    
1085     def servicefilter(service_name):
1086         service = service_name
1087         def fltr(e):
1088             return e['serviceName'] == service
1089         return fltr
1090         
1091     
1092     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1093     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1094     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1095     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1096         
1097     return data    
1098
1099 @signed_terms_required
1100 @login_required
1101 def timeline(request):
1102     data = {'entity':request.user.email}
1103     timeline_body = ()
1104     timeline_header = ()
1105     form = TimelineForm(data)
1106     if request.method == 'POST':
1107         data = request.POST
1108         form = TimelineForm(data)
1109         if form.is_valid():
1110             data = form.cleaned_data
1111             timeline_header = ('entity', 'resource',
1112                                'event name', 'event date',
1113                                'incremental cost', 'total cost')
1114             timeline_body = timeline_charge(
1115                                     data['entity'],     data['resource'],
1116                                     data['start_date'], data['end_date'],
1117                                     data['details'],    data['operation'])
1118         
1119     return render_response(template='im/timeline.html',
1120                            context_instance=get_context(request),
1121                            form=form,
1122                            timeline_header=timeline_header,
1123                            timeline_body=timeline_body)
1124                            l=l)