rename astakosgroup_form_demo.html to astakosgroup_form.html
[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.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             try:
937                 map(obj.approve_member, addmembers_form.valid_users)
938             except AssertionError:
939                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
940                 messages.error(request, msg)
941             addmembers_form = AddGroupMembersForm()
942
943     template_name = "%s/%s_detail.html" % (
944         model._meta.app_label, model._meta.object_name.lower())
945     t = template_loader.get_template(template_name)
946     c = RequestContext(request, {
947         'object': obj,
948     }, context_processors)
949
950     # validate sorting
951     sorting = request.GET.get('sorting')
952     if sorting:
953         form = MembersSortForm({'sort_by': sorting})
954         if form.is_valid():
955             sorting = form.cleaned_data.get('sort_by')
956
957     extra_context = {'update_form': update_form,
958                      'addmembers_form': addmembers_form,
959                      'page': request.GET.get('page', 1),
960                      'sorting': sorting}
961     for key, value in extra_context.items():
962         if callable(value):
963             c[key] = value()
964         else:
965             c[key] = value
966     response = HttpResponse(t.render(c), mimetype=mimetype)
967     populate_xheaders(
968         request, response, model, getattr(obj, obj._meta.pk.name))
969     return response
970
971
972 @require_http_methods(["GET", "POST"])
973 @signed_terms_required
974 @login_required
975 def group_search(request, extra_context=None, **kwargs):
976     q = request.GET.get('q')
977     sorting = request.GET.get('sorting')
978     if request.method == 'GET':
979         form = AstakosGroupSearchForm({'q': q} if q else None)
980     else:
981         form = AstakosGroupSearchForm(get_query(request))
982         if form.is_valid():
983             q = form.cleaned_data['q'].strip()
984     if q:
985         queryset = AstakosGroup.objects.select_related()
986         queryset = queryset.filter(name__contains=q)
987         queryset = queryset.filter(approval_date__isnull=False)
988         queryset = queryset.extra(select={
989                                   'groupname': DB_REPLACE_GROUP_SCHEME,
990                                   'kindname': "im_groupkind.name",
991                                   'approved_members_num': """
992                     SELECT COUNT(*) FROM im_membership
993                     WHERE group_id = im_astakosgroup.group_ptr_id
994                     AND date_joined IS NOT NULL""",
995                                   'membership_approval_date': """
996                     SELECT date_joined FROM im_membership
997                     WHERE group_id = im_astakosgroup.group_ptr_id
998                     AND person_id = %s""" % request.user.id,
999                                   'is_member': """
1000                     SELECT CASE WHEN EXISTS(
1001                     SELECT date_joined FROM im_membership
1002                     WHERE group_id = im_astakosgroup.group_ptr_id
1003                     AND person_id = %s)
1004                     THEN 1 ELSE 0 END""" % request.user.id,
1005                                   'is_owner': """
1006                     SELECT CASE WHEN EXISTS(
1007                     SELECT id FROM im_astakosuser_owner
1008                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1009                     AND astakosuser_id = %s)
1010                     THEN 1 ELSE 0 END""" % request.user.id})
1011         if sorting:
1012             # TODO check sorting value
1013             queryset = queryset.order_by(sorting)
1014     else:
1015         queryset = AstakosGroup.objects.none()
1016     return object_list(
1017         request,
1018         queryset,
1019         paginate_by=PAGINATE_BY,
1020         page=request.GET.get('page') or 1,
1021         template_name='im/astakosgroup_list.html',
1022         extra_context=dict(form=form,
1023                            is_search=True,
1024                            q=q,
1025                            sorting=sorting))
1026
1027
1028 @require_http_methods(["GET", "POST"])
1029 @signed_terms_required
1030 @login_required
1031 def group_all(request, extra_context=None, **kwargs):
1032     q = AstakosGroup.objects.select_related()
1033     q = q.filter(approval_date__isnull=False)
1034     q = q.extra(select={
1035                 'groupname': DB_REPLACE_GROUP_SCHEME,
1036                 'kindname': "im_groupkind.name",
1037                 'approved_members_num': """
1038                     SELECT COUNT(*) FROM im_membership
1039                     WHERE group_id = im_astakosgroup.group_ptr_id
1040                     AND date_joined IS NOT NULL""",
1041                 'membership_approval_date': """
1042                     SELECT date_joined FROM im_membership
1043                     WHERE group_id = im_astakosgroup.group_ptr_id
1044                     AND person_id = %s""" % request.user.id,
1045                 'is_member': """
1046                     SELECT CASE WHEN EXISTS(
1047                     SELECT date_joined FROM im_membership
1048                     WHERE group_id = im_astakosgroup.group_ptr_id
1049                     AND person_id = %s)
1050                     THEN 1 ELSE 0 END""" % request.user.id})
1051     sorting = request.GET.get('sorting')
1052     if sorting:
1053         # TODO check sorting value
1054         q = q.order_by(sorting)
1055     return object_list(
1056         request,
1057         q,
1058         paginate_by=PAGINATE_BY,
1059         page=request.GET.get('page') or 1,
1060         template_name='im/astakosgroup_list.html',
1061         extra_context=dict(form=AstakosGroupSearchForm(),
1062                            is_search=True,
1063                            sorting=sorting))
1064
1065
1066 #@require_http_methods(["POST"])
1067 @require_http_methods(["POST", "GET"])
1068 @signed_terms_required
1069 @login_required
1070 def group_join(request, group_id):
1071     m = Membership(group_id=group_id,
1072                    person=request.user,
1073                    date_requested=datetime.now())
1074     try:
1075         m.save()
1076         post_save_redirect = reverse(
1077             'group_detail',
1078             kwargs=dict(group_id=group_id))
1079         return HttpResponseRedirect(post_save_redirect)
1080     except IntegrityError, e:
1081         logger.exception(e)
1082         msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1083         messages.error(request, msg)
1084         return group_search(request)
1085
1086
1087 @require_http_methods(["POST"])
1088 @signed_terms_required
1089 @login_required
1090 def group_leave(request, group_id):
1091     try:
1092         m = Membership.objects.select_related().get(
1093             group__id=group_id,
1094             person=request.user)
1095     except Membership.DoesNotExist:
1096         return HttpResponseBadRequest(_(astakos_messages.NOT_A_MEMBER))
1097     if request.user in m.group.owner.all():
1098         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1099     return delete_object(
1100         request,
1101         model=Membership,
1102         object_id=m.id,
1103         template_name='im/astakosgroup_list.html',
1104         post_delete_redirect=reverse(
1105             'group_detail',
1106             kwargs=dict(group_id=group_id)))
1107
1108
1109 def handle_membership(func):
1110     @wraps(func)
1111     def wrapper(request, group_id, user_id):
1112         try:
1113             m = Membership.objects.select_related().get(
1114                 group__id=group_id,
1115                 person__id=user_id)
1116         except Membership.DoesNotExist:
1117             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1118         else:
1119             if request.user not in m.group.owner.all():
1120                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1121             func(request, m)
1122             return group_detail(request, group_id)
1123     return wrapper
1124
1125
1126 #@require_http_methods(["POST"])
1127 @require_http_methods(["POST", "GET"])
1128 @signed_terms_required
1129 @login_required
1130 @handle_membership
1131 def approve_member(request, membership):
1132     try:
1133         membership.approve()
1134         realname = membership.person.realname
1135         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1136         messages.success(request, msg)
1137     except AssertionError:
1138         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1139         messages.error(request, msg)
1140     except BaseException, e:
1141         logger.exception(e)
1142         realname = membership.person.realname
1143         msg = _(astakos_messages.GENERIC_ERROR)
1144         messages.error(request, msg)
1145
1146
1147 @signed_terms_required
1148 @login_required
1149 @handle_membership
1150 def disapprove_member(request, membership):
1151     try:
1152         membership.disapprove()
1153         realname = membership.person.realname
1154         msg = MEMBER_REMOVED % realname
1155         messages.success(request, msg)
1156     except BaseException, e:
1157         logger.exception(e)
1158         msg = _(astakos_messages.GENERIC_ERROR)
1159         messages.error(request, msg)
1160
1161
1162 #@require_http_methods(["GET"])
1163 @require_http_methods(["POST", "GET"])
1164 @signed_terms_required
1165 @login_required
1166 def resource_list(request):
1167 #     if request.method == 'POST':
1168 #         form = PickResourceForm(request.POST)
1169 #         if form.is_valid():
1170 #             r = form.cleaned_data.get('resource')
1171 #             if r:
1172 #                 groups = request.user.membership_set.only('group').filter(
1173 #                     date_joined__isnull=False)
1174 #                 groups = [g.group_id for g in groups]
1175 #                 q = AstakosGroupQuota.objects.select_related().filter(
1176 #                     resource=r, group__in=groups)
1177 #     else:
1178 #         form = PickResourceForm()
1179 #         q = AstakosGroupQuota.objects.none()
1180 #
1181 #     return object_list(request, q,
1182 #                        template_name='im/astakosuserquota_list.html',
1183 #                        extra_context={'form': form, 'data':data})
1184
1185     def with_class(entry):
1186         entry['load_class'] = 'red'
1187         max_value = float(entry['maxValue'])
1188         curr_value = float(entry['currValue'])
1189         if max_value > 0 :
1190            entry['ratio'] = (curr_value / max_value) * 100
1191         else: 
1192            entry['ratio'] = 0 
1193         if entry['ratio'] < 66:
1194             entry['load_class'] = 'yellow'
1195         if entry['ratio'] < 33:
1196             entry['load_class'] = 'green'
1197         return entry
1198
1199     def pluralize(entry):
1200         entry['plural'] = engine.plural(entry.get('name'))
1201         return entry
1202
1203     result = callpoint.get_user_status(request.user.id)
1204     if result.is_success:
1205         backenddata = map(with_class, result.data)
1206         data = map(pluralize, result.data)
1207     else:
1208         data = None
1209         messages.error(request, result.reason)
1210     return render_response('im/resource_list.html',
1211                            data=data,
1212                            resource_presentation=resource_presentation,
1213                            context_instance=get_context(request))
1214
1215
1216 def group_create_list(request):
1217     form = PickResourceForm()
1218     return render_response(
1219         template='im/astakosgroup_create_list.html',
1220         context_instance=get_context(request),)
1221
1222
1223 #@require_http_methods(["GET"])
1224 @require_http_methods(["POST", "GET"])
1225 @signed_terms_required
1226 @login_required
1227 def billing(request):
1228
1229     today = datetime.today()
1230     month_last_day = calendar.monthrange(today.year, today.month)[1]
1231     data['resources'] = map(with_class, data['resources'])
1232     start = request.POST.get('datefrom', None)
1233     if start:
1234         today = datetime.fromtimestamp(int(start))
1235         month_last_day = calendar.monthrange(today.year, today.month)[1]
1236
1237     start = datetime(today.year, today.month, 1).strftime("%s")
1238     end = datetime(today.year, today.month, month_last_day).strftime("%s")
1239     r = request_billing.apply(args=('pgerakios@grnet.gr',
1240                                     int(start) * 1000,
1241                                     int(end) * 1000))
1242     data = {}
1243
1244     try:
1245         status, data = r.result
1246         data = _clear_billing_data(data)
1247         if status != 200:
1248             messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1249     except:
1250         messages.error(request, r.result)
1251
1252     print type(start)
1253
1254     return render_response(
1255         template='im/billing.html',
1256         context_instance=get_context(request),
1257         data=data,
1258         zerodate=datetime(month=1, year=1970, day=1),
1259         today=today,
1260         start=int(start),
1261         month_last_day=month_last_day)
1262
1263
1264 def _clear_billing_data(data):
1265
1266     # remove addcredits entries
1267     def isnotcredit(e):
1268         return e['serviceName'] != "addcredits"
1269
1270     # separate services
1271     def servicefilter(service_name):
1272         service = service_name
1273
1274         def fltr(e):
1275             return e['serviceName'] == service
1276         return fltr
1277
1278     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1279     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1280     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1281     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1282
1283     return data
1284      
1285      
1286 #@require_http_methods(["GET"])
1287 @require_http_methods(["POST", "GET"])
1288 @signed_terms_required
1289 @login_required
1290 def timeline(request):
1291 #    data = {'entity':request.user.email}
1292     timeline_body = ()
1293     timeline_header = ()
1294 #    form = TimelineForm(data)
1295     form = TimelineForm()
1296     if request.method == 'POST':
1297         data = request.POST
1298         form = TimelineForm(data)
1299         if form.is_valid():
1300             data = form.cleaned_data
1301             timeline_header = ('entity', 'resource',
1302                                'event name', 'event date',
1303                                'incremental cost', 'total cost')
1304             timeline_body = timeline_charge(
1305                 data['entity'], data['resource'],
1306                 data['start_date'], data['end_date'],
1307                 data['details'], data['operation'])
1308
1309     return render_response(template='im/timeline.html',
1310                            context_instance=get_context(request),
1311                            form=form,
1312                            timeline_header=timeline_header,
1313                            timeline_body=timeline_body)
1314     return data