Back up file
[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':'Pithos+ Diskspace',
686             'placeholder':'eg. 10GB'
687         },
688         'cyclades.vm': {
689             'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
690             'is_abbreviation':True,
691             'report_desc':'Virtual Machines',
692             'placeholder':'eg. 2'
693         },
694         'cyclades.disksize': {
695             'help_text':'resource cyclades.disksize help text',
696             'is_abbreviation':False,
697             'report_desc':'Disksize',
698             'placeholder':'eg. 5GB'
699         },
700         'cyclades.disk': {
701             'help_text':'resource cyclades.disk help text',
702             'is_abbreviation':False,
703             'report_desc':'Disk',
704             'placeholder':'eg. 5GB'    
705         },
706         'cyclades.ram': {
707             'help_text':'resource cyclades.ram help text',
708             'is_abbreviation':True,
709             'report_desc':'RAM',
710             'placeholder':'eg. 4GB'
711         },
712         'cyclades.cpu': {
713             'help_text':'resource cyclades.cpu help text',
714             'is_abbreviation':True,
715             'report_desc':'CPUs',
716             'placeholder':'eg. 1'
717         },
718         'cyclades.network.private': {
719             'help_text':'resource cyclades.network.private help text',
720             'is_abbreviation':False,
721             'report_desc':'Network',
722             'placeholder':'eg. 1'
723         }
724     }
725
726 @require_http_methods(["GET", "POST"])
727 @signed_terms_required
728 @login_required
729 def group_add(request, kind_name='default'):
730     result = callpoint.list_resources()
731     resource_catalog = {'resources':defaultdict(defaultdict),
732                         'groups':defaultdict(list)}
733     if result.is_success:
734         for r in result.data:
735             service = r.get('service', '')
736             name = r.get('name', '')
737             group = r.get('group', '')
738             unit = r.get('unit', '')
739             fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
740             resource_catalog['resources'][fullname] = dict(unit=unit)
741             resource_catalog['groups'][group].append(fullname)
742         
743         resource_catalog = dict(resource_catalog)
744         for k, v in resource_catalog.iteritems():
745             resource_catalog[k] = dict(v)
746     else:
747         messages.error(
748             request,
749             'Unable to retrieve system resources: %s' % result.reason
750     )
751     
752     try:
753         kind = GroupKind.objects.get(name=kind_name)
754     except:
755         return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
756     
757     
758
759     post_save_redirect = '/im/group/%(id)s/'
760     context_processors = None
761     model, form_class = get_model_and_form_class(
762         model=None,
763         form_class=AstakosGroupCreationForm
764     )
765     
766     if request.method == 'POST':
767         form = form_class(request.POST, request.FILES)
768         if form.is_valid():
769             return render_response(
770                 template='im/astakosgroup_form_summary.html',
771                 context_instance=get_context(request),
772                 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
773                 policies = form.policies(),
774                 resource_catalog=resource_catalog,
775                 resource_presentation=resource_presentation
776             )
777     else:
778         now = datetime.now()
779         data = {
780             'kind': kind
781         }
782         form = form_class(data)
783
784     # Create the template, context, response
785     template_name = "%s/%s_form.html" % (
786         model._meta.app_label,
787         model._meta.object_name.lower()
788     )
789     t = template_loader.get_template(template_name)
790     c = RequestContext(request, {
791         'form': form,
792         'kind': kind,
793         'resource_catalog':resource_catalog,
794         'resource_presentation':resource_presentation,
795     }, context_processors)
796     return HttpResponse(t.render(c))
797
798
799 #@require_http_methods(["POST"])
800 @require_http_methods(["GET", "POST"])
801 @signed_terms_required
802 @login_required
803 def group_add_complete(request):
804     model = AstakosGroup
805     form = AstakosGroupCreationSummaryForm(request.POST)
806     if form.is_valid():
807         d = form.cleaned_data
808         d['owners'] = [request.user]
809         result = callpoint.create_groups((d,)).next()
810         if result.is_success:
811             new_object = result.data[0]
812             msg = _(astakos_messages.OBJECT_CREATED) %\
813                 {"verbose_name": model._meta.verbose_name}
814             messages.success(request, msg, fail_silently=True)
815             
816             # send notification
817             try:
818                 send_group_creation_notification(
819                     template_name='im/group_creation_notification.txt',
820                     dictionary={
821                         'group': new_object,
822                         'owner': request.user,
823                         'policies': d.get('policies', [])
824                     }
825                 )
826             except SendNotificationError, e:
827                 messages.error(request, e, fail_silently=True)
828             post_save_redirect = '/im/group/%(id)s/'
829             return HttpResponseRedirect(post_save_redirect % new_object)
830         else:
831             msg = _(astakos_messages.OBJECT_CREATED_FAILED) %\
832                 {"verbose_name": model._meta.verbose_name,
833                  "reason":result.reason}
834             messages.error(request, msg, fail_silently=True)
835     return render_response(
836         template='im/astakosgroup_form_summary.html',
837         context_instance=get_context(request),
838         form=form)
839
840
841 #@require_http_methods(["GET"])
842 @require_http_methods(["GET", "POST"])
843 @signed_terms_required
844 @login_required
845 def group_list(request):
846     none = request.user.astakos_groups.none()
847     q = AstakosGroup.objects.raw("""
848         SELECT auth_group.id,
849         %s AS groupname,
850         im_groupkind.name AS kindname,
851         im_astakosgroup.*,
852         owner.email AS groupowner,
853         (SELECT COUNT(*) FROM im_membership
854             WHERE group_id = im_astakosgroup.group_ptr_id
855             AND date_joined IS NOT NULL) AS approved_members_num,
856         (SELECT CASE WHEN(
857                     SELECT date_joined FROM im_membership
858                     WHERE group_id = im_astakosgroup.group_ptr_id
859                     AND person_id = %s) IS NULL
860                     THEN 0 ELSE 1 END) AS membership_status
861         FROM im_astakosgroup
862         INNER JOIN im_membership ON (
863             im_astakosgroup.group_ptr_id = im_membership.group_id)
864         INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
865         INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
866         LEFT JOIN im_astakosuser_owner ON (
867             im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
868         LEFT JOIN auth_user as owner ON (
869             im_astakosuser_owner.astakosuser_id = owner.id)
870         WHERE im_membership.person_id = %s
871         """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
872     d = defaultdict(list)
873     
874     for g in q:
875         if request.user.email == g.groupowner:
876             d['own'].append(g)
877         else:
878             d['other'].append(g)
879     
880     for g in q:
881         d['all'].append(g)
882     
883     # validate sorting
884     fields = ('own', 'other', 'all')
885     for f in fields:
886         v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
887         if v:
888             form = AstakosGroupSortForm({'sort_by': v})
889             if not form.is_valid():
890                 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
891     return object_list(request, queryset=none,
892                        extra_context={'is_search': False,
893                                       'all': d['all'],
894                                       'mine': d['own'],
895                                       'other': d['other'],
896                                       'own_sorting': own_sorting,
897                                       'other_sorting': other_sorting,
898                                       'all_sorting': all_sorting,
899                                       'own_page': request.GET.get('own_page', 1),
900                                       'other_page': request.GET.get('other_page', 1),
901                                       'all_page': request.GET.get('all_page', 1)
902                                       })
903
904
905 @require_http_methods(["GET", "POST"])
906 @signed_terms_required
907 @login_required
908 def group_detail(request, group_id):
909     q = AstakosGroup.objects.select_related().filter(pk=group_id)
910     q = q.extra(select={
911         'is_member': """SELECT CASE WHEN EXISTS(
912                             SELECT id FROM im_membership
913                             WHERE group_id = im_astakosgroup.group_ptr_id
914                             AND person_id = %s)
915                         THEN 1 ELSE 0 END""" % request.user.id,
916         'is_owner': """SELECT CASE WHEN EXISTS(
917                         SELECT id FROM im_astakosuser_owner
918                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
919                         AND astakosuser_id = %s)
920                         THEN 1 ELSE 0 END""" % request.user.id,
921         'kindname': """SELECT name FROM im_groupkind
922                        WHERE id = im_astakosgroup.kind_id"""})
923
924     model = q.model
925     context_processors = None
926     mimetype = None
927     try:
928         obj = q.get()
929     except AstakosGroup.DoesNotExist:
930         raise Http404("No %s found matching the query" % (
931             model._meta.verbose_name))
932
933     update_form = AstakosGroupUpdateForm(instance=obj)
934     addmembers_form = AddGroupMembersForm()
935     if request.method == 'POST':
936         update_data = {}
937         addmembers_data = {}
938         for k, v in request.POST.iteritems():
939             if k in update_form.fields:
940                 update_data[k] = v
941             if k in addmembers_form.fields:
942                 addmembers_data[k] = v
943         update_data = update_data or None
944         addmembers_data = addmembers_data or None
945         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
946         addmembers_form = AddGroupMembersForm(addmembers_data)
947         if update_form.is_valid():
948             update_form.save()
949         if addmembers_form.is_valid():
950             try:
951                 map(obj.approve_member, addmembers_form.valid_users)
952             except AssertionError:
953                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
954                 messages.error(request, msg)
955             addmembers_form = AddGroupMembersForm()
956
957     template_name = "%s/%s_detail.html" % (
958         model._meta.app_label, model._meta.object_name.lower())
959     t = template_loader.get_template(template_name)
960     c = RequestContext(request, {
961         'object': obj,
962     }, context_processors)
963
964     # validate sorting
965     sorting = request.GET.get('sorting')
966     if sorting:
967         form = MembersSortForm({'sort_by': sorting})
968         if form.is_valid():
969             sorting = form.cleaned_data.get('sort_by')
970
971     extra_context = {'update_form': update_form,
972                      'addmembers_form': addmembers_form,
973                      'page': request.GET.get('page', 1),
974                      'sorting': sorting}
975     for key, value in extra_context.items():
976         if callable(value):
977             c[key] = value()
978         else:
979             c[key] = value
980     response = HttpResponse(t.render(c), mimetype=mimetype)
981     populate_xheaders(
982         request, response, model, getattr(obj, obj._meta.pk.name))
983     return response
984
985
986 @require_http_methods(["GET", "POST"])
987 @signed_terms_required
988 @login_required
989 def group_search(request, extra_context=None, **kwargs):
990     q = request.GET.get('q')
991     sorting = request.GET.get('sorting')
992     if request.method == 'GET':
993         form = AstakosGroupSearchForm({'q': q} if q else None)
994     else:
995         form = AstakosGroupSearchForm(get_query(request))
996         if form.is_valid():
997             q = form.cleaned_data['q'].strip()
998     if q:
999         queryset = AstakosGroup.objects.select_related()
1000         queryset = queryset.filter(name__contains=q)
1001         queryset = queryset.filter(approval_date__isnull=False)
1002         queryset = queryset.extra(select={
1003                                   'groupname': DB_REPLACE_GROUP_SCHEME,
1004                                   'kindname': "im_groupkind.name",
1005                                   'approved_members_num': """
1006                     SELECT COUNT(*) FROM im_membership
1007                     WHERE group_id = im_astakosgroup.group_ptr_id
1008                     AND date_joined IS NOT NULL""",
1009                                   'membership_approval_date': """
1010                     SELECT date_joined FROM im_membership
1011                     WHERE group_id = im_astakosgroup.group_ptr_id
1012                     AND person_id = %s""" % request.user.id,
1013                                   'is_member': """
1014                     SELECT CASE WHEN EXISTS(
1015                     SELECT date_joined FROM im_membership
1016                     WHERE group_id = im_astakosgroup.group_ptr_id
1017                     AND person_id = %s)
1018                     THEN 1 ELSE 0 END""" % request.user.id,
1019                                   'is_owner': """
1020                     SELECT CASE WHEN EXISTS(
1021                     SELECT id FROM im_astakosuser_owner
1022                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1023                     AND astakosuser_id = %s)
1024                     THEN 1 ELSE 0 END""" % request.user.id})
1025         if sorting:
1026             # TODO check sorting value
1027             queryset = queryset.order_by(sorting)
1028     else:
1029         queryset = AstakosGroup.objects.none()
1030     return object_list(
1031         request,
1032         queryset,
1033         paginate_by=PAGINATE_BY,
1034         page=request.GET.get('page') or 1,
1035         template_name='im/astakosgroup_list.html',
1036         extra_context=dict(form=form,
1037                            is_search=True,
1038                            q=q,
1039                            sorting=sorting))
1040
1041
1042 @require_http_methods(["GET", "POST"])
1043 @signed_terms_required
1044 @login_required
1045 def group_all(request, extra_context=None, **kwargs):
1046     q = AstakosGroup.objects.select_related()
1047     q = q.filter(approval_date__isnull=False)
1048     q = q.extra(select={
1049                 'groupname': DB_REPLACE_GROUP_SCHEME,
1050                 'kindname': "im_groupkind.name",
1051                 'approved_members_num': """
1052                     SELECT COUNT(*) FROM im_membership
1053                     WHERE group_id = im_astakosgroup.group_ptr_id
1054                     AND date_joined IS NOT NULL""",
1055                 'membership_approval_date': """
1056                     SELECT date_joined FROM im_membership
1057                     WHERE group_id = im_astakosgroup.group_ptr_id
1058                     AND person_id = %s""" % request.user.id,
1059                 'is_member': """
1060                     SELECT CASE WHEN EXISTS(
1061                     SELECT date_joined FROM im_membership
1062                     WHERE group_id = im_astakosgroup.group_ptr_id
1063                     AND person_id = %s)
1064                     THEN 1 ELSE 0 END""" % request.user.id})
1065     sorting = request.GET.get('sorting')
1066     if sorting:
1067         # TODO check sorting value
1068         q = q.order_by(sorting)
1069     return object_list(
1070         request,
1071         q,
1072         paginate_by=PAGINATE_BY,
1073         page=request.GET.get('page') or 1,
1074         template_name='im/astakosgroup_list.html',
1075         extra_context=dict(form=AstakosGroupSearchForm(),
1076                            is_search=True,
1077                            sorting=sorting))
1078
1079
1080 #@require_http_methods(["POST"])
1081 @require_http_methods(["POST", "GET"])
1082 @signed_terms_required
1083 @login_required
1084 def group_join(request, group_id):
1085     m = Membership(group_id=group_id,
1086                    person=request.user,
1087                    date_requested=datetime.now())
1088     try:
1089         m.save()
1090         post_save_redirect = reverse(
1091             'group_detail',
1092             kwargs=dict(group_id=group_id))
1093         return HttpResponseRedirect(post_save_redirect)
1094     except IntegrityError, e:
1095         logger.exception(e)
1096         msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1097         messages.error(request, msg)
1098         return group_search(request)
1099
1100
1101 @require_http_methods(["POST"])
1102 @signed_terms_required
1103 @login_required
1104 def group_leave(request, group_id):
1105     try:
1106         m = Membership.objects.select_related().get(
1107             group__id=group_id,
1108             person=request.user)
1109     except Membership.DoesNotExist:
1110         return HttpResponseBadRequest(_(astakos_messages.NOT_A_MEMBER))
1111     if request.user in m.group.owner.all():
1112         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1113     return delete_object(
1114         request,
1115         model=Membership,
1116         object_id=m.id,
1117         template_name='im/astakosgroup_list.html',
1118         post_delete_redirect=reverse(
1119             'group_detail',
1120             kwargs=dict(group_id=group_id)))
1121
1122
1123 def handle_membership(func):
1124     @wraps(func)
1125     def wrapper(request, group_id, user_id):
1126         try:
1127             m = Membership.objects.select_related().get(
1128                 group__id=group_id,
1129                 person__id=user_id)
1130         except Membership.DoesNotExist:
1131             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1132         else:
1133             if request.user not in m.group.owner.all():
1134                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1135             func(request, m)
1136             return group_detail(request, group_id)
1137     return wrapper
1138
1139
1140 #@require_http_methods(["POST"])
1141 @require_http_methods(["POST", "GET"])
1142 @signed_terms_required
1143 @login_required
1144 @handle_membership
1145 def approve_member(request, membership):
1146     try:
1147         membership.approve()
1148         realname = membership.person.realname
1149         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1150         messages.success(request, msg)
1151     except AssertionError:
1152         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1153         messages.error(request, msg)
1154     except BaseException, e:
1155         logger.exception(e)
1156         realname = membership.person.realname
1157         msg = _(astakos_messages.GENERIC_ERROR)
1158         messages.error(request, msg)
1159
1160
1161 @signed_terms_required
1162 @login_required
1163 @handle_membership
1164 def disapprove_member(request, membership):
1165     try:
1166         membership.disapprove()
1167         realname = membership.person.realname
1168         msg = MEMBER_REMOVED % realname
1169         messages.success(request, msg)
1170     except BaseException, e:
1171         logger.exception(e)
1172         msg = _(astakos_messages.GENERIC_ERROR)
1173         messages.error(request, msg)
1174
1175
1176 #@require_http_methods(["GET"])
1177 @require_http_methods(["POST", "GET"])
1178 @signed_terms_required
1179 @login_required
1180 def resource_list(request):
1181 #     if request.method == 'POST':
1182 #         form = PickResourceForm(request.POST)
1183 #         if form.is_valid():
1184 #             r = form.cleaned_data.get('resource')
1185 #             if r:
1186 #                 groups = request.user.membership_set.only('group').filter(
1187 #                     date_joined__isnull=False)
1188 #                 groups = [g.group_id for g in groups]
1189 #                 q = AstakosGroupQuota.objects.select_related().filter(
1190 #                     resource=r, group__in=groups)
1191 #     else:
1192 #         form = PickResourceForm()
1193 #         q = AstakosGroupQuota.objects.none()
1194 #
1195 #     return object_list(request, q,
1196 #                        template_name='im/astakosuserquota_list.html',
1197 #                        extra_context={'form': form, 'data':data})
1198
1199     def with_class(entry):
1200         entry['load_class'] = 'red'
1201         max_value = float(entry['maxValue'])
1202         curr_value = float(entry['currValue'])
1203         if max_value > 0 :
1204            entry['ratio'] = (curr_value / max_value) * 100
1205         else: 
1206            entry['ratio'] = 0 
1207         if entry['ratio'] < 66:
1208             entry['load_class'] = 'yellow'
1209         if entry['ratio'] < 33:
1210             entry['load_class'] = 'green'
1211         return entry
1212
1213     def pluralize(entry):
1214         entry['plural'] = engine.plural(entry.get('name'))
1215         return entry
1216
1217     result = callpoint.get_user_status(request.user.id)
1218     if result.is_success:
1219         backenddata = map(with_class, result.data)
1220         data = map(pluralize, result.data)
1221     else:
1222         data = None
1223         messages.error(request, result.reason)
1224     return render_response('im/resource_list.html',
1225                            data=data,
1226                            resource_presentation=resource_presentation,
1227                            context_instance=get_context(request))
1228
1229
1230 def group_create_list(request):
1231     form = PickResourceForm()
1232     return render_response(
1233         template='im/astakosgroup_create_list.html',
1234         context_instance=get_context(request),)
1235
1236
1237 #@require_http_methods(["GET"])
1238 @require_http_methods(["POST", "GET"])
1239 @signed_terms_required
1240 @login_required
1241 def billing(request):
1242
1243     today = datetime.today()
1244     month_last_day = calendar.monthrange(today.year, today.month)[1]
1245     data['resources'] = map(with_class, data['resources'])
1246     start = request.POST.get('datefrom', None)
1247     if start:
1248         today = datetime.fromtimestamp(int(start))
1249         month_last_day = calendar.monthrange(today.year, today.month)[1]
1250
1251     start = datetime(today.year, today.month, 1).strftime("%s")
1252     end = datetime(today.year, today.month, month_last_day).strftime("%s")
1253     r = request_billing.apply(args=('pgerakios@grnet.gr',
1254                                     int(start) * 1000,
1255                                     int(end) * 1000))
1256     data = {}
1257
1258     try:
1259         status, data = r.result
1260         data = _clear_billing_data(data)
1261         if status != 200:
1262             messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1263     except:
1264         messages.error(request, r.result)
1265
1266     print type(start)
1267
1268     return render_response(
1269         template='im/billing.html',
1270         context_instance=get_context(request),
1271         data=data,
1272         zerodate=datetime(month=1, year=1970, day=1),
1273         today=today,
1274         start=int(start),
1275         month_last_day=month_last_day)
1276
1277
1278 def _clear_billing_data(data):
1279
1280     # remove addcredits entries
1281     def isnotcredit(e):
1282         return e['serviceName'] != "addcredits"
1283
1284     # separate services
1285     def servicefilter(service_name):
1286         service = service_name
1287
1288         def fltr(e):
1289             return e['serviceName'] == service
1290         return fltr
1291
1292     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1293     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1294     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1295     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1296
1297     return data
1298      
1299      
1300 #@require_http_methods(["GET"])
1301 @require_http_methods(["POST", "GET"])
1302 @signed_terms_required
1303 @login_required
1304 def timeline(request):
1305 #    data = {'entity':request.user.email}
1306     timeline_body = ()
1307     timeline_header = ()
1308 #    form = TimelineForm(data)
1309     form = TimelineForm()
1310     if request.method == 'POST':
1311         data = request.POST
1312         form = TimelineForm(data)
1313         if form.is_valid():
1314             data = form.cleaned_data
1315             timeline_header = ('entity', 'resource',
1316                                'event name', 'event date',
1317                                'incremental cost', 'total cost')
1318             timeline_body = timeline_charge(
1319                 data['entity'], data['resource'],
1320                 data['start_date'], data['end_date'],
1321                 data['details'], data['operation'])
1322
1323     return render_response(template='im/timeline.html',
1324                            context_instance=get_context(request),
1325                            form=form,
1326                            timeline_header=timeline_header,
1327                            timeline_body=timeline_body)
1328     return data