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