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