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