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