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