1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
38 engine = inflect.engine()
40 from urllib import quote
41 from functools import wraps
42 from datetime import datetime, timedelta
43 from collections import defaultdict
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
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
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 AstakosGroupCreationSummaryForm)
81 from astakos.im.functions import (send_feedback, SendMailError,
82 logout as auth_logout,
83 activate as activate_func,
84 switch_account_to_shibboleth,
85 send_group_creation_notification,
86 SendNotificationError)
87 from astakos.im.endpoints.quotaholder import timeline_charge
88 from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
89 LOGGING_LEVEL, PAGINATE_BY)
90 from astakos.im.tasks import request_billing
91 from astakos.im.api.callpoint import AstakosCallpoint
93 logger = logging.getLogger(__name__)
96 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
99 callpoint = AstakosCallpoint()
101 def render_response(template, tab=None, status=200, reset_cookie=False,
102 context_instance=None, **kwargs):
104 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
105 keyword argument and returns an ``django.http.HttpResponse`` with the
106 specified ``status``.
109 tab = template.partition('_')[0].partition('.html')[0]
110 kwargs.setdefault('tab', tab)
111 html = template_loader.render_to_string(
112 template, kwargs, context_instance=context_instance)
113 response = HttpResponse(html, status=status)
115 set_cookie(response, context_instance['request'].user)
119 def requires_anonymous(func):
121 Decorator checkes whether the request.user is not Anonymous and in that case
122 redirects to `logout`.
125 def wrapper(request, *args):
126 if not request.user.is_anonymous():
127 next = urlencode({'next': request.build_absolute_uri()})
128 logout_uri = reverse(logout) + '?' + next
129 return HttpResponseRedirect(logout_uri)
130 return func(request, *args)
134 def signed_terms_required(func):
136 Decorator checkes whether the request.user is Anonymous and in that case
137 redirects to `logout`.
140 def wrapper(request, *args, **kwargs):
141 if request.user.is_authenticated() and not request.user.signed_terms:
142 params = urlencode({'next': request.build_absolute_uri(),
144 terms_uri = reverse('latest_terms') + '?' + params
145 return HttpResponseRedirect(terms_uri)
146 return func(request, *args, **kwargs)
150 @require_http_methods(["GET", "POST"])
151 @signed_terms_required
152 def index(request, login_template_name='im/login.html', extra_context=None):
154 If there is logged on user renders the profile page otherwise renders login page.
158 ``login_template_name``
159 A custom login template to use. This is optional; if not specified,
160 this will default to ``im/login.html``.
162 ``profile_template_name``
163 A custom profile template to use. This is optional; if not specified,
164 this will default to ``im/profile.html``.
167 An dictionary of variables to add to the template context.
171 im/profile.html or im/login.html or ``template_name`` keyword argument.
174 template_name = login_template_name
175 if request.user.is_authenticated():
176 return HttpResponseRedirect(reverse('edit_profile'))
177 return render_response(template_name,
178 login_form=LoginForm(request=request),
179 context_instance=get_context(request, extra_context))
182 @require_http_methods(["GET", "POST"])
184 @signed_terms_required
185 @transaction.commit_manually
186 def invite(request, template_name='im/invitations.html', extra_context=None):
188 Allows a user to invite somebody else.
190 In case of GET request renders a form for providing the invitee information.
191 In case of POST checks whether the user has not run out of invitations and then
192 sends an invitation email to singup to the service.
194 The view uses commit_manually decorator in order to ensure the number of the
195 user invitations is going to be updated only if the email has been successfully sent.
197 If the user isn't logged in, redirects to settings.LOGIN_URL.
202 A custom template to use. This is optional; if not specified,
203 this will default to ``im/invitations.html``.
206 An dictionary of variables to add to the template context.
210 im/invitations.html or ``template_name`` keyword argument.
214 The view expectes the following settings are defined:
216 * LOGIN_URL: login uri
217 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
221 form = InvitationForm()
223 inviter = request.user
224 if request.method == 'POST':
225 form = InvitationForm(request.POST)
226 if inviter.invitations > 0:
229 email = form.cleaned_data.get('username')
230 realname = form.cleaned_data.get('realname')
231 inviter.invite(email, realname)
232 message = _('Invitation sent to %s' % email)
233 messages.success(request, message)
234 except SendMailError, e:
236 messages.error(request, message)
237 transaction.rollback()
238 except BaseException, e:
239 message = _('Something went wrong.')
240 messages.error(request, message)
242 transaction.rollback()
246 message = _('No invitations left')
247 messages.error(request, message)
249 sent = [{'email': inv.username,
250 'realname': inv.realname,
251 'is_consumed': inv.is_consumed}
252 for inv in request.user.invitations_sent.all()]
253 kwargs = {'inviter': inviter,
255 context = get_context(request, extra_context, **kwargs)
256 return render_response(template_name,
257 invitation_form=form,
258 context_instance=context)
261 @require_http_methods(["GET", "POST"])
263 @signed_terms_required
264 def edit_profile(request, template_name='im/profile.html', extra_context=None):
266 Allows a user to edit his/her profile.
268 In case of GET request renders a form for displaying the user information.
269 In case of POST updates the user informantion and redirects to ``next``
270 url parameter if exists.
272 If the user isn't logged in, redirects to settings.LOGIN_URL.
277 A custom template to use. This is optional; if not specified,
278 this will default to ``im/profile.html``.
281 An dictionary of variables to add to the template context.
285 im/profile.html or ``template_name`` keyword argument.
289 The view expectes the following settings are defined:
291 * LOGIN_URL: login uri
293 extra_context = extra_context or {}
294 form = ProfileForm(instance=request.user)
295 extra_context['next'] = request.GET.get('next')
297 if request.method == 'POST':
298 form = ProfileForm(request.POST, instance=request.user)
301 prev_token = request.user.auth_token
303 reset_cookie = user.auth_token != prev_token
304 form = ProfileForm(instance=user)
305 next = request.POST.get('next')
307 return redirect(next)
308 msg = _('Profile has been updated successfully')
309 messages.success(request, msg)
310 except ValueError, ve:
311 messages.success(request, ve)
312 elif request.method == "GET":
313 if not request.user.is_verified:
314 request.user.is_verified = True
316 return render_response(template_name,
317 reset_cookie=reset_cookie,
319 context_instance=get_context(request,
323 @transaction.commit_manually
324 @require_http_methods(["GET", "POST"])
325 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
327 Allows a user to create a local account.
329 In case of GET request renders a form for entering the user information.
330 In case of POST handles the signup.
332 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
333 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
334 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
335 (see activation_backends);
337 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
338 otherwise renders the same page with a success message.
340 On unsuccessful creation, renders ``template_name`` with an error message.
345 A custom template to render. This is optional;
346 if not specified, this will default to ``im/signup.html``.
349 A custom template to render in case of success. This is optional;
350 if not specified, this will default to ``im/signup_complete.html``.
353 An dictionary of variables to add to the template context.
357 im/signup.html or ``template_name`` keyword argument.
358 im/signup_complete.html or ``on_success`` keyword argument.
360 if request.user.is_authenticated():
361 return HttpResponseRedirect(reverse('edit_profile'))
363 provider = get_query(request).get('provider', 'local')
366 backend = get_backend(request)
367 form = backend.get_signup_form(provider)
369 form = SimpleBackend(request).get_signup_form(provider)
370 messages.error(request, e)
371 if request.method == 'POST':
373 user = form.save(commit=False)
375 result = backend.handle_activation(user)
376 status = messages.SUCCESS
377 message = result.message
379 if 'additional_email' in form.cleaned_data:
380 additional_email = form.cleaned_data['additional_email']
381 if additional_email != user.email:
382 user.additionalmail_set.create(email=additional_email)
383 msg = 'Additional email: %s saved for user %s.' % (
384 additional_email, user.email)
385 logger.log(LOGGING_LEVEL, msg)
386 if user and user.is_active:
387 next = request.POST.get('next', '')
388 response = prepare_response(request, user, next=next)
391 messages.add_message(request, status, message)
393 return render_response(on_success,
394 context_instance=get_context(request, extra_context))
395 except SendMailError, e:
397 messages.error(request, message)
398 transaction.rollback()
399 except BaseException, e:
400 message = _('Something went wrong.')
401 messages.error(request, message)
403 transaction.rollback()
404 return render_response(template_name,
407 context_instance=get_context(request, extra_context))
410 @require_http_methods(["GET", "POST"])
412 @signed_terms_required
413 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
415 Allows a user to send feedback.
417 In case of GET request renders a form for providing the feedback information.
418 In case of POST sends an email to support team.
420 If the user isn't logged in, redirects to settings.LOGIN_URL.
425 A custom template to use. This is optional; if not specified,
426 this will default to ``im/feedback.html``.
429 An dictionary of variables to add to the template context.
433 im/signup.html or ``template_name`` keyword argument.
437 * LOGIN_URL: login uri
438 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
440 if request.method == 'GET':
441 form = FeedbackForm()
442 if request.method == 'POST':
444 return HttpResponse('Unauthorized', status=401)
446 form = FeedbackForm(request.POST)
448 msg = form.cleaned_data['feedback_msg']
449 data = form.cleaned_data['feedback_data']
451 send_feedback(msg, data, request.user, email_template_name)
452 except SendMailError, e:
453 messages.error(request, message)
455 message = _('Feedback successfully sent')
456 messages.success(request, message)
457 return render_response(template_name,
459 context_instance=get_context(request, extra_context))
462 @require_http_methods(["GET", "POST"])
463 @signed_terms_required
464 def logout(request, template='registration/logged_out.html', extra_context=None):
466 Wraps `django.contrib.auth.logout` and delete the cookie.
468 response = HttpResponse()
469 if request.user.is_authenticated():
470 email = request.user.email
472 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
473 msg = 'Cookie deleted for %s' % email
474 logger.log(LOGGING_LEVEL, msg)
475 next = request.GET.get('next')
477 response['Location'] = next
478 response.status_code = 302
481 response['Location'] = LOGOUT_NEXT
482 response.status_code = 301
484 messages.success(request, _('You have successfully logged out.'))
485 context = get_context(request, extra_context)
487 template_loader.render_to_string(template, context_instance=context))
491 @require_http_methods(["GET", "POST"])
492 @transaction.commit_manually
493 def activate(request, greeting_email_template_name='im/welcome_email.txt',
494 helpdesk_email_template_name='im/helpdesk_notification.txt'):
496 Activates the user identified by the ``auth`` request parameter, sends a welcome email
497 and renews the user token.
499 The view uses commit_manually decorator in order to ensure the user state will be updated
500 only if the email will be send successfully.
502 token = request.GET.get('auth')
503 next = request.GET.get('next')
505 user = AstakosUser.objects.get(auth_token=token)
506 except AstakosUser.DoesNotExist:
507 return HttpResponseBadRequest(_('No such user'))
510 message = _('Account already active.')
511 messages.error(request, message)
512 return index(request)
515 local_user = AstakosUser.objects.get(
520 except AstakosUser.DoesNotExist:
524 greeting_email_template_name,
525 helpdesk_email_template_name,
528 response = prepare_response(request, user, next, renew=True)
531 except SendMailError, e:
533 messages.error(request, message)
534 transaction.rollback()
535 return index(request)
536 except BaseException, e:
537 message = _('Something went wrong.')
538 messages.error(request, message)
540 transaction.rollback()
541 return index(request)
544 user = switch_account_to_shibboleth(
547 greeting_email_template_name
549 response = prepare_response(request, user, next, renew=True)
552 except SendMailError, e:
554 messages.error(request, message)
555 transaction.rollback()
556 return index(request)
557 except BaseException, e:
558 message = _('Something went wrong.')
559 messages.error(request, message)
561 transaction.rollback()
562 return index(request)
565 @require_http_methods(["GET", "POST"])
566 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
571 term = ApprovalTerms.objects.order_by('-id')[0]
576 term = ApprovalTerms.objects.get(id=term_id)
577 except ApprovalTerms.DoesNotExist, e:
581 messages.error(request, 'There are no approval terms.')
582 return HttpResponseRedirect(reverse('index'))
583 f = open(term.location, 'r')
586 if request.method == 'POST':
587 next = request.POST.get('next')
589 next = reverse('index')
590 form = SignApprovalTermsForm(request.POST, instance=request.user)
591 if not form.is_valid():
592 return render_response(template_name,
594 approval_terms_form=form,
595 context_instance=get_context(request, extra_context))
597 return HttpResponseRedirect(next)
600 if request.user.is_authenticated() and not request.user.signed_terms:
601 form = SignApprovalTermsForm(instance=request.user)
602 return render_response(template_name,
604 approval_terms_form=form,
605 context_instance=get_context(request, extra_context))
608 @require_http_methods(["GET", "POST"])
609 @signed_terms_required
610 def change_password(request):
611 return password_change(request,
612 post_change_redirect=reverse('edit_profile'),
613 password_change_form=ExtendedPasswordChangeForm)
616 @require_http_methods(["GET", "POST"])
617 @signed_terms_required
619 @transaction.commit_manually
620 def change_email(request, activation_key=None,
621 email_template_name='registration/email_change_email.txt',
622 form_template_name='registration/email_change_form.html',
623 confirm_template_name='registration/email_change_done.html',
627 user = EmailChange.objects.change_email(activation_key)
628 if request.user.is_authenticated() and request.user == user:
629 msg = _('Email changed successfully.')
630 messages.success(request, msg)
632 response = prepare_response(request, user)
635 except ValueError, e:
636 messages.error(request, e)
637 return render_response(confirm_template_name,
638 modified_user=user if 'user' in locals(
640 context_instance=get_context(request,
643 if not request.user.is_authenticated():
644 path = quote(request.get_full_path())
645 url = request.build_absolute_uri(reverse('index'))
646 return HttpResponseRedirect(url + '?next=' + path)
647 form = EmailChangeForm(request.POST or None)
648 if request.method == 'POST' and form.is_valid():
650 ec = form.save(email_template_name, request)
651 except SendMailError, e:
653 messages.error(request, msg)
654 transaction.rollback()
655 except IntegrityError, e:
656 msg = _('There is already a pending change email request.')
657 messages.error(request, msg)
659 msg = _('Change email request has been registered succefully.\
660 You are going to receive a verification email in the new address.')
661 messages.success(request, msg)
663 return render_response(form_template_name,
665 context_instance=get_context(request,
670 resource_presentation = {
672 'help_text':'group compute help text',
673 'is_abbreviation':False,
677 'help_text':'group storage help text',
678 'is_abbreviation':False,
681 'pithos+.diskspace': {
682 'help_text':'resource pithos+.diskspace help text',
683 'is_abbreviation':False,
684 'report_desc':'Pithos+ Diskspace',
685 'placeholder':'eg. 10GB'
688 'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
689 'is_abbreviation':True,
690 'report_desc':'Virtual Machines',
691 'placeholder':'eg. 2'
693 'cyclades.disksize': {
694 'help_text':'resource cyclades.disksize help text',
695 'is_abbreviation':False,
696 'report_desc':'Disksize',
697 'placeholder':'eg. 5GB'
700 'help_text':'resource cyclades.disk help text',
701 'is_abbreviation':False,
702 'report_desc':'Disk',
703 'placeholder':'eg. 5GB'
706 'help_text':'resource cyclades.ram help text',
707 'is_abbreviation':True,
709 'placeholder':'eg. 4GB'
712 'help_text':'resource cyclades.cpu help text',
713 'is_abbreviation':True,
714 'report_desc':'CPUs',
715 'placeholder':'eg. 1'
717 'cyclades.network.private': {
718 'help_text':'resource cyclades.network.private help text',
719 'is_abbreviation':False,
720 'report_desc':'Network',
721 'placeholder':'eg. 1'
725 @require_http_methods(["GET", "POST"])
726 @signed_terms_required
728 def group_add(request, kind_name='default'):
729 result = callpoint.list_resources()
730 resource_catalog = {'resources':defaultdict(defaultdict),
731 'groups':defaultdict(list)}
732 if result.is_success:
733 for r in result.data:
734 service = r.get('service', '')
735 name = r.get('name', '')
736 group = r.get('group', '')
737 unit = r.get('unit', '')
738 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
739 resource_catalog['resources'][fullname] = dict(unit=unit)
740 resource_catalog['groups'][group].append(fullname)
742 resource_catalog = dict(resource_catalog)
743 for k, v in resource_catalog.iteritems():
744 resource_catalog[k] = dict(v)
748 'Unable to retrieve system resources: %s' % result.reason
752 kind = GroupKind.objects.get(name=kind_name)
754 return HttpResponseBadRequest(_('No such group kind'))
758 post_save_redirect = '/im/group/%(id)s/'
759 context_processors = None
760 model, form_class = get_model_and_form_class(
762 form_class=AstakosGroupCreationForm
765 if request.method == 'POST':
766 form = form_class(request.POST, request.FILES)
768 return render_response(
769 template='im/astakosgroup_form_summary.html',
770 context_instance=get_context(request),
771 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
772 policies = form.policies(),
773 resource_catalog=resource_catalog,
774 resource_presentation=resource_presentation
781 form = form_class(data)
783 # Create the template, context, response
784 template_name = "%s/%s_form_demo.html" % (
785 model._meta.app_label,
786 model._meta.object_name.lower()
788 t = template_loader.get_template(template_name)
789 c = RequestContext(request, {
792 'resource_catalog':resource_catalog,
793 'resource_presentation':resource_presentation,
794 }, context_processors)
795 return HttpResponse(t.render(c))
798 @require_http_methods(["POST"])
799 @signed_terms_required
801 def group_add_complete(request):
803 form = AstakosGroupCreationSummaryForm(request.POST)
805 d = form.cleaned_data
806 d['owners'] = [request.user]
807 result = callpoint.create_groups((d,)).next()
808 if result.is_success:
809 new_object = result.data[0]
810 msg = _("The %(verbose_name)s was created successfully.") %\
811 {"verbose_name": model._meta.verbose_name}
812 messages.success(request, msg, fail_silently=True)
816 send_group_creation_notification(
817 template_name='im/group_creation_notification.txt',
820 'owner': request.user,
821 'policies': d.get('policies', [])
824 except SendNotificationError, e:
825 messages.error(request, e, fail_silently=True)
826 post_save_redirect = '/im/group/%(id)s/'
827 return HttpResponseRedirect(post_save_redirect % new_object)
829 msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
830 {"verbose_name": model._meta.verbose_name,
831 "reason":result.reason}
832 messages.error(request, msg, fail_silently=True)
833 return render_response(
834 template='im/astakosgroup_form_summary.html',
835 context_instance=get_context(request),
839 @require_http_methods(["GET"])
840 @signed_terms_required
842 def group_list(request):
843 none = request.user.astakos_groups.none()
844 q = AstakosGroup.objects.raw("""
845 SELECT auth_group.id,
847 im_groupkind.name AS kindname,
849 owner.email AS groupowner,
850 (SELECT COUNT(*) FROM im_membership
851 WHERE group_id = im_astakosgroup.group_ptr_id
852 AND date_joined IS NOT NULL) AS approved_members_num,
854 SELECT date_joined FROM im_membership
855 WHERE group_id = im_astakosgroup.group_ptr_id
856 AND person_id = %s) IS NULL
857 THEN 0 ELSE 1 END) AS membership_status
859 INNER JOIN im_membership ON (
860 im_astakosgroup.group_ptr_id = im_membership.group_id)
861 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
862 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
863 LEFT JOIN im_astakosuser_owner ON (
864 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
865 LEFT JOIN auth_user as owner ON (
866 im_astakosuser_owner.astakosuser_id = owner.id)
867 WHERE im_membership.person_id = %s
868 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
869 d = defaultdict(list)
872 if request.user.email == g.groupowner:
881 fields = ('own', 'other', 'all')
883 v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
885 form = AstakosGroupSortForm({'sort_by': v})
886 if not form.is_valid():
887 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
888 return object_list(request, queryset=none,
889 extra_context={'is_search': False,
893 'own_sorting': own_sorting,
894 'other_sorting': other_sorting,
895 'all_sorting': all_sorting,
896 'own_page': request.GET.get('own_page', 1),
897 'other_page': request.GET.get('other_page', 1),
898 'all_page': request.GET.get('all_page', 1)
902 @require_http_methods(["GET", "POST"])
903 @signed_terms_required
905 def group_detail(request, group_id):
906 q = AstakosGroup.objects.select_related().filter(pk=group_id)
908 'is_member': """SELECT CASE WHEN EXISTS(
909 SELECT id FROM im_membership
910 WHERE group_id = im_astakosgroup.group_ptr_id
912 THEN 1 ELSE 0 END""" % request.user.id,
913 'is_owner': """SELECT CASE WHEN EXISTS(
914 SELECT id FROM im_astakosuser_owner
915 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
916 AND astakosuser_id = %s)
917 THEN 1 ELSE 0 END""" % request.user.id,
918 'kindname': """SELECT name FROM im_groupkind
919 WHERE id = im_astakosgroup.kind_id"""})
922 context_processors = None
926 except AstakosGroup.DoesNotExist:
927 raise Http404("No %s found matching the query" % (
928 model._meta.verbose_name))
930 update_form = AstakosGroupUpdateForm(instance=obj)
931 addmembers_form = AddGroupMembersForm()
932 if request.method == 'POST':
935 for k, v in request.POST.iteritems():
936 if k in update_form.fields:
938 if k in addmembers_form.fields:
939 addmembers_data[k] = v
940 update_data = update_data or None
941 addmembers_data = addmembers_data or None
942 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
943 addmembers_form = AddGroupMembersForm(addmembers_data)
944 if update_form.is_valid():
946 if addmembers_form.is_valid():
947 map(obj.approve_member, addmembers_form.valid_users)
948 addmembers_form = AddGroupMembersForm()
950 template_name = "%s/%s_detail.html" % (
951 model._meta.app_label, model._meta.object_name.lower())
952 t = template_loader.get_template(template_name)
953 c = RequestContext(request, {
955 }, context_processors)
958 sorting = request.GET.get('sorting')
960 form = MembersSortForm({'sort_by': sorting})
962 sorting = form.cleaned_data.get('sort_by')
964 extra_context = {'update_form': update_form,
965 'addmembers_form': addmembers_form,
966 'page': request.GET.get('page', 1),
968 for key, value in extra_context.items():
973 response = HttpResponse(t.render(c), mimetype=mimetype)
975 request, response, model, getattr(obj, obj._meta.pk.name))
979 @require_http_methods(["GET", "POST"])
980 @signed_terms_required
982 def group_search(request, extra_context=None, **kwargs):
983 q = request.GET.get('q')
984 sorting = request.GET.get('sorting')
985 if request.method == 'GET':
986 form = AstakosGroupSearchForm({'q': q} if q else None)
988 form = AstakosGroupSearchForm(get_query(request))
990 q = form.cleaned_data['q'].strip()
992 queryset = AstakosGroup.objects.select_related()
993 queryset = queryset.filter(name__contains=q)
994 queryset = queryset.filter(approval_date__isnull=False)
995 queryset = queryset.extra(select={
996 'groupname': DB_REPLACE_GROUP_SCHEME,
997 'kindname': "im_groupkind.name",
998 'approved_members_num': """
999 SELECT COUNT(*) FROM im_membership
1000 WHERE group_id = im_astakosgroup.group_ptr_id
1001 AND date_joined IS NOT NULL""",
1002 'membership_approval_date': """
1003 SELECT date_joined FROM im_membership
1004 WHERE group_id = im_astakosgroup.group_ptr_id
1005 AND person_id = %s""" % request.user.id,
1007 SELECT CASE WHEN EXISTS(
1008 SELECT date_joined FROM im_membership
1009 WHERE group_id = im_astakosgroup.group_ptr_id
1011 THEN 1 ELSE 0 END""" % request.user.id,
1013 SELECT CASE WHEN EXISTS(
1014 SELECT id FROM im_astakosuser_owner
1015 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1016 AND astakosuser_id = %s)
1017 THEN 1 ELSE 0 END""" % request.user.id})
1019 # TODO check sorting value
1020 queryset = queryset.order_by(sorting)
1022 queryset = AstakosGroup.objects.none()
1026 paginate_by=PAGINATE_BY,
1027 page=request.GET.get('page') or 1,
1028 template_name='im/astakosgroup_list.html',
1029 extra_context=dict(form=form,
1035 @require_http_methods(["POST"])
1036 @signed_terms_required
1038 def group_all(request, extra_context=None, **kwargs):
1039 q = AstakosGroup.objects.select_related()
1040 q = q.filter(approval_date__isnull=False)
1041 q = q.extra(select={
1042 'groupname': DB_REPLACE_GROUP_SCHEME,
1043 'kindname': "im_groupkind.name",
1044 'approved_members_num': """
1045 SELECT COUNT(*) FROM im_membership
1046 WHERE group_id = im_astakosgroup.group_ptr_id
1047 AND date_joined IS NOT NULL""",
1048 'membership_approval_date': """
1049 SELECT date_joined FROM im_membership
1050 WHERE group_id = im_astakosgroup.group_ptr_id
1051 AND person_id = %s""" % request.user.id,
1053 SELECT CASE WHEN EXISTS(
1054 SELECT date_joined FROM im_membership
1055 WHERE group_id = im_astakosgroup.group_ptr_id
1057 THEN 1 ELSE 0 END""" % request.user.id})
1058 sorting = request.GET.get('sorting')
1060 # TODO check sorting value
1061 q = q.order_by(sorting)
1065 paginate_by=PAGINATE_BY,
1066 page=request.GET.get('page') or 1,
1067 template_name='im/astakosgroup_list.html',
1068 extra_context=dict(form=AstakosGroupSearchForm(),
1073 @require_http_methods(["POST"])
1074 @signed_terms_required
1076 def group_join(request, group_id):
1077 m = Membership(group_id=group_id,
1078 person=request.user,
1079 date_requested=datetime.now())
1082 post_save_redirect = reverse(
1084 kwargs=dict(group_id=group_id))
1085 return HttpResponseRedirect(post_save_redirect)
1086 except IntegrityError, e:
1088 msg = _('Failed to join group.')
1089 messages.error(request, msg)
1090 return group_search(request)
1093 @require_http_methods(["POST"])
1094 @signed_terms_required
1096 def group_leave(request, group_id):
1098 m = Membership.objects.select_related().get(
1100 person=request.user)
1101 except Membership.DoesNotExist:
1102 return HttpResponseBadRequest(_('Invalid membership.'))
1103 if request.user in m.group.owner.all():
1104 return HttpResponseForbidden(_('Owner can not leave the group.'))
1105 return delete_object(
1109 template_name='im/astakosgroup_list.html',
1110 post_delete_redirect=reverse(
1112 kwargs=dict(group_id=group_id)))
1115 def handle_membership(func):
1117 def wrapper(request, group_id, user_id):
1119 m = Membership.objects.select_related().get(
1122 except Membership.DoesNotExist:
1123 return HttpResponseBadRequest(_('Invalid membership.'))
1125 if request.user not in m.group.owner.all():
1126 return HttpResponseForbidden(_('User is not a group owner.'))
1128 return group_detail(request, group_id)
1132 @require_http_methods(["POST"])
1133 @signed_terms_required
1136 def approve_member(request, membership):
1138 membership.approve()
1139 realname = membership.person.realname
1140 msg = _('%s has been successfully joined the group.' % realname)
1141 messages.success(request, msg)
1142 except BaseException, e:
1144 realname = membership.person.realname
1145 msg = _('Something went wrong during %s\'s approval.' % realname)
1146 messages.error(request, msg)
1149 @signed_terms_required
1152 def disapprove_member(request, membership):
1154 membership.disapprove()
1155 realname = membership.person.realname
1156 msg = _('%s has been successfully removed from the group.' % realname)
1157 messages.success(request, msg)
1158 except BaseException, e:
1160 msg = _('Something went wrong during %s\'s disapproval.' % realname)
1161 messages.error(request, msg)
1164 @require_http_methods(["GET"])
1165 @signed_terms_required
1167 def resource_list(request):
1168 # if request.method == 'POST':
1169 # form = PickResourceForm(request.POST)
1170 # if form.is_valid():
1171 # r = form.cleaned_data.get('resource')
1173 # groups = request.user.membership_set.only('group').filter(
1174 # date_joined__isnull=False)
1175 # groups = [g.group_id for g in groups]
1176 # q = AstakosGroupQuota.objects.select_related().filter(
1177 # resource=r, group__in=groups)
1179 # form = PickResourceForm()
1180 # q = AstakosGroupQuota.objects.none()
1182 # return object_list(request, q,
1183 # template_name='im/astakosuserquota_list.html',
1184 # extra_context={'form': form, 'data':data})
1186 def with_class(entry):
1187 entry['load_class'] = 'red'
1188 max_value = float(entry['maxValue'])
1189 curr_value = float(entry['currValue'])
1191 entry['ratio'] = (curr_value / max_value) * 100
1194 if entry['ratio'] < 66:
1195 entry['load_class'] = 'yellow'
1196 if entry['ratio'] < 33:
1197 entry['load_class'] = 'green'
1200 def pluralize(entry):
1201 entry['plural'] = engine.plural(entry.get('name'))
1204 result = callpoint.get_user_status(request.user.id)
1205 if result.is_success:
1206 backenddata = map(with_class, result.data)
1207 data = map(pluralize, result.data)
1210 messages.error(request, result.reason)
1211 return render_response('im/resource_list.html',
1213 resource_presentation=resource_presentation,
1214 context_instance=get_context(request))
1217 def group_create_list(request):
1218 form = PickResourceForm()
1219 return render_response(
1220 template='im/astakosgroup_create_list.html',
1221 context_instance=get_context(request),)
1224 @require_http_methods(["GET"])
1225 @signed_terms_required
1227 def billing(request):
1229 today = datetime.today()
1230 month_last_day = calendar.monthrange(today.year, today.month)[1]
1231 data['resources'] = map(with_class, data['resources'])
1232 start = request.POST.get('datefrom', None)
1234 today = datetime.fromtimestamp(int(start))
1235 month_last_day = calendar.monthrange(today.year, today.month)[1]
1237 start = datetime(today.year, today.month, 1).strftime("%s")
1238 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1239 r = request_billing.apply(args=('pgerakios@grnet.gr',
1245 status, data = r.result
1246 data = _clear_billing_data(data)
1248 messages.error(request, _('Service response status: %d' % status))
1250 messages.error(request, r.result)
1254 return render_response(
1255 template='im/billing.html',
1256 context_instance=get_context(request),
1258 zerodate=datetime(month=1, year=1970, day=1),
1261 month_last_day=month_last_day)
1264 def _clear_billing_data(data):
1266 # remove addcredits entries
1268 return e['serviceName'] != "addcredits"
1271 def servicefilter(service_name):
1272 service = service_name
1275 return e['serviceName'] == service
1278 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1279 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1280 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1281 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1286 @require_http_methods(["GET"])
1287 @signed_terms_required
1289 def timeline(request):
1290 # data = {'entity':request.user.email}
1292 timeline_header = ()
1293 # form = TimelineForm(data)
1294 form = TimelineForm()
1295 if request.method == 'POST':
1297 form = TimelineForm(data)
1299 data = form.cleaned_data
1300 timeline_header = ('entity', 'resource',
1301 'event name', 'event date',
1302 'incremental cost', 'total cost')
1303 timeline_body = timeline_charge(
1304 data['entity'], data['resource'],
1305 data['start_date'], data['end_date'],
1306 data['details'], data['operation'])
1308 return render_response(template='im/timeline.html',
1309 context_instance=get_context(request),
1311 timeline_header=timeline_header,
1312 timeline_body=timeline_body)