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