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