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