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