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