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