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
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 from django.core.exceptions import ValidationError, PermissionDenied
62 from django.template.loader import render_to_string
63 from django.views.decorators.http import require_http_methods
64 from astakos.im.activation_backends import get_backend, SimpleBackend
66 from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
67 EmailChange, GroupKind, Membership,
68 RESOURCE_SEPARATOR, AstakosUserAuthProvider)
69 from astakos.im.util import get_context, prepare_response, get_query, restrict_next
70 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
71 FeedbackForm, SignApprovalTermsForm,
73 AstakosGroupCreationForm, AstakosGroupSearchForm,
74 AstakosGroupUpdateForm, AddGroupMembersForm,
75 MembersSortForm, AstakosGroupSortForm,
76 TimelineForm, PickResourceForm,
77 AstakosGroupCreationSummaryForm)
78 from astakos.im.functions import (send_feedback, SendMailError,
79 logout as auth_logout,
80 activate as activate_func,
81 send_activation as send_activation_func,
82 send_group_creation_notification,
83 SendNotificationError)
84 from astakos.im.endpoints.qh import timeline_charge
85 from astakos.im.settings import (COOKIE_DOMAIN, LOGOUT_NEXT,
86 LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
87 from astakos.im.tasks import request_billing
88 from astakos.im.api.callpoint import AstakosCallpoint
90 import astakos.im.messages as astakos_messages
91 from astakos.im import settings
92 from astakos.im import auth_providers
94 logger = logging.getLogger(__name__)
96 callpoint = AstakosCallpoint()
98 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
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``.
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)
112 def requires_auth_provider(provider_id, **perms):
115 def decorator(func, *args, **kwargs):
117 def wrapper(request, *args, **kwargs):
118 provider = auth_providers.get_provider(provider_id)
120 if not provider or not provider.is_active():
121 raise PermissionDenied
124 for pkey, value in perms.iteritems():
125 attr = 'is_available_for_%s' % pkey.lower()
126 if getattr(provider, attr)() != value:
127 raise PermissionDenied
128 return func(request, *args)
133 def requires_anonymous(func):
135 Decorator checkes whether the request.user is not Anonymous and in that case
136 redirects to `logout`.
139 def wrapper(request, *args):
140 if not request.user.is_anonymous():
141 next = urlencode({'next': request.build_absolute_uri()})
142 logout_uri = reverse(logout) + '?' + next
143 return HttpResponseRedirect(logout_uri)
144 return func(request, *args)
148 def signed_terms_required(func):
150 Decorator checkes whether the request.user is Anonymous and in that case
151 redirects to `logout`.
154 def wrapper(request, *args, **kwargs):
155 if request.user.is_authenticated() and not request.user.signed_terms:
156 params = urlencode({'next': request.build_absolute_uri(),
158 terms_uri = reverse('latest_terms') + '?' + params
159 return HttpResponseRedirect(terms_uri)
160 return func(request, *args, **kwargs)
164 @require_http_methods(["GET", "POST"])
165 @signed_terms_required
166 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
168 If there is logged on user renders the profile page otherwise renders login page.
172 ``login_template_name``
173 A custom login template to use. This is optional; if not specified,
174 this will default to ``im/login.html``.
176 ``profile_template_name``
177 A custom profile template to use. This is optional; if not specified,
178 this will default to ``im/profile.html``.
181 An dictionary of variables to add to the template context.
185 im/profile.html or im/login.html or ``template_name`` keyword argument.
188 extra_context = extra_context or {}
189 template_name = login_template_name
190 if request.user.is_authenticated():
191 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
193 return render_response(
195 login_form = LoginForm(request=request),
196 context_instance = get_context(request, extra_context)
200 @require_http_methods(["GET", "POST"])
202 @signed_terms_required
203 @transaction.commit_manually
204 def invite(request, template_name='im/invitations.html', extra_context=None):
206 Allows a user to invite somebody else.
208 In case of GET request renders a form for providing the invitee information.
209 In case of POST checks whether the user has not run out of invitations and then
210 sends an invitation email to singup to the service.
212 The view uses commit_manually decorator in order to ensure the number of the
213 user invitations is going to be updated only if the email has been successfully sent.
215 If the user isn't logged in, redirects to settings.LOGIN_URL.
220 A custom template to use. This is optional; if not specified,
221 this will default to ``im/invitations.html``.
224 An dictionary of variables to add to the template context.
228 im/invitations.html or ``template_name`` keyword argument.
232 The view expectes the following settings are defined:
234 * LOGIN_URL: login uri
236 extra_context = extra_context or {}
239 form = InvitationForm()
241 inviter = request.user
242 if request.method == 'POST':
243 form = InvitationForm(request.POST)
244 if inviter.invitations > 0:
247 email = form.cleaned_data.get('username')
248 realname = form.cleaned_data.get('realname')
249 inviter.invite(email, realname)
250 message = _(astakos_messages.INVITATION_SENT) % locals()
251 messages.success(request, message)
252 except SendMailError, e:
254 messages.error(request, message)
255 transaction.rollback()
256 except BaseException, e:
257 message = _(astakos_messages.GENERIC_ERROR)
258 messages.error(request, message)
260 transaction.rollback()
264 message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
265 messages.error(request, message)
267 sent = [{'email': inv.username,
268 'realname': inv.realname,
269 'is_consumed': inv.is_consumed}
270 for inv in request.user.invitations_sent.all()]
271 kwargs = {'inviter': inviter,
273 context = get_context(request, extra_context, **kwargs)
274 return render_response(template_name,
275 invitation_form=form,
276 context_instance=context)
279 @require_http_methods(["GET", "POST"])
281 @signed_terms_required
282 def edit_profile(request, template_name='im/profile.html', extra_context=None):
284 Allows a user to edit his/her profile.
286 In case of GET request renders a form for displaying the user information.
287 In case of POST updates the user informantion and redirects to ``next``
288 url parameter if exists.
290 If the user isn't logged in, redirects to settings.LOGIN_URL.
295 A custom template to use. This is optional; if not specified,
296 this will default to ``im/profile.html``.
299 An dictionary of variables to add to the template context.
303 im/profile.html or ``template_name`` keyword argument.
307 The view expectes the following settings are defined:
309 * LOGIN_URL: login uri
311 extra_context = extra_context or {}
313 instance=request.user,
314 session_key=request.session.session_key
316 extra_context['next'] = request.GET.get('next')
317 if request.method == 'POST':
320 instance=request.user,
321 session_key=request.session.session_key
325 prev_token = request.user.auth_token
329 session_key=request.session.session_key
331 next = restrict_next(
332 request.POST.get('next'),
336 return redirect(next)
337 msg = _(astakos_messages.PROFILE_UPDATED)
338 messages.success(request, msg)
339 except ValueError, ve:
340 messages.success(request, ve)
341 elif request.method == "GET":
342 request.user.is_verified = True
346 user_providers = request.user.get_active_auth_providers()
348 # providers that user can add
349 user_available_providers = request.user.get_available_auth_providers()
351 return render_response(template_name,
353 user_providers = user_providers,
354 user_available_providers = user_available_providers,
355 context_instance = get_context(request,
359 @transaction.commit_manually
360 @require_http_methods(["GET", "POST"])
361 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
363 Allows a user to create a local account.
365 In case of GET request renders a form for entering the user information.
366 In case of POST handles the signup.
368 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
369 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
370 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
371 (see activation_backends);
373 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
374 otherwise renders the same page with a success message.
376 On unsuccessful creation, renders ``template_name`` with an error message.
381 A custom template to render. This is optional;
382 if not specified, this will default to ``im/signup.html``.
385 A custom template to render in case of success. This is optional;
386 if not specified, this will default to ``im/signup_complete.html``.
389 An dictionary of variables to add to the template context.
393 im/signup.html or ``template_name`` keyword argument.
394 im/signup_complete.html or ``on_success`` keyword argument.
396 extra_context = extra_context or {}
397 if request.user.is_authenticated():
398 return HttpResponseRedirect(reverse('edit_profile'))
400 provider = get_query(request).get('provider', 'local')
401 if not auth_providers.get_provider(provider).is_available_for_create():
402 raise PermissionDenied
404 id = get_query(request).get('id')
406 instance = AstakosUser.objects.get(id=id) if id else None
407 except AstakosUser.DoesNotExist:
410 third_party_token = request.REQUEST.get('third_party_token', None)
414 backend = get_backend(request)
415 form = backend.get_signup_form(provider, instance)
417 form = SimpleBackend(request).get_signup_form(provider)
418 messages.error(request, e)
419 if request.method == 'POST':
421 user = form.save(commit=False)
423 result = backend.handle_activation(user)
424 status = messages.SUCCESS
425 message = result.message
427 form.store_user(user, request)
429 if 'additional_email' in form.cleaned_data:
430 additional_email = form.cleaned_data['additional_email']
431 if additional_email != user.email:
432 user.additionalmail_set.create(email=additional_email)
433 msg = 'Additional email: %s saved for user %s.' % (
437 logger._log(LOGGING_LEVEL, msg, [])
438 if user and user.is_active:
439 next = request.POST.get('next', '')
440 response = prepare_response(request, user, next=next)
443 messages.add_message(request, status, message)
445 return render_response(
447 context_instance=get_context(
452 except SendMailError, e:
454 status = messages.ERROR
456 messages.error(request, message)
457 transaction.rollback()
458 except BaseException, e:
460 message = _(astakos_messages.GENERIC_ERROR)
461 messages.error(request, message)
463 transaction.rollback()
464 return render_response(template_name,
466 third_party_token=third_party_token,
468 context_instance=get_context(request, extra_context))
471 @require_http_methods(["GET", "POST"])
473 @signed_terms_required
474 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
476 Allows a user to send feedback.
478 In case of GET request renders a form for providing the feedback information.
479 In case of POST sends an email to support team.
481 If the user isn't logged in, redirects to settings.LOGIN_URL.
486 A custom template to use. This is optional; if not specified,
487 this will default to ``im/feedback.html``.
490 An dictionary of variables to add to the template context.
494 im/signup.html or ``template_name`` keyword argument.
498 * LOGIN_URL: login uri
500 extra_context = extra_context or {}
501 if request.method == 'GET':
502 form = FeedbackForm()
503 if request.method == 'POST':
505 return HttpResponse('Unauthorized', status=401)
507 form = FeedbackForm(request.POST)
509 msg = form.cleaned_data['feedback_msg']
510 data = form.cleaned_data['feedback_data']
512 send_feedback(msg, data, request.user, email_template_name)
513 except SendMailError, e:
514 messages.error(request, message)
516 message = _(astakos_messages.FEEDBACK_SENT)
517 messages.success(request, message)
518 return render_response(template_name,
520 context_instance=get_context(request, extra_context))
523 @require_http_methods(["GET"])
524 @signed_terms_required
525 def logout(request, template='registration/logged_out.html', extra_context=None):
527 Wraps `django.contrib.auth.logout`.
529 extra_context = extra_context or {}
530 response = HttpResponse()
531 if request.user.is_authenticated():
532 email = request.user.email
534 next = restrict_next(
535 request.GET.get('next'),
539 response['Location'] = next
540 response.status_code = 302
542 response['Location'] = LOGOUT_NEXT
543 response.status_code = 301
545 messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
546 context = get_context(request, extra_context)
547 response.write(render_to_string(template, context_instance=context))
551 @require_http_methods(["GET", "POST"])
552 @transaction.commit_manually
553 def activate(request, greeting_email_template_name='im/welcome_email.txt',
554 helpdesk_email_template_name='im/helpdesk_notification.txt'):
556 Activates the user identified by the ``auth`` request parameter, sends a welcome email
557 and renews the user token.
559 The view uses commit_manually decorator in order to ensure the user state will be updated
560 only if the email will be send successfully.
562 token = request.GET.get('auth')
563 next = request.GET.get('next')
565 user = AstakosUser.objects.get(auth_token=token)
566 except AstakosUser.DoesNotExist:
567 return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
570 message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
571 messages.error(request, message)
572 return index(request)
575 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
576 response = prepare_response(request, user, next, renew=True)
579 except SendMailError, e:
581 messages.add_message(request, messages.ERROR, message)
582 transaction.rollback()
583 return index(request)
584 except BaseException, e:
585 status = messages.ERROR
586 message = _(astakos_messages.GENERIC_ERROR)
587 messages.add_message(request, messages.ERROR, message)
589 transaction.rollback()
590 return index(request)
593 @require_http_methods(["GET", "POST"])
594 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
595 extra_context = extra_context or {}
600 term = ApprovalTerms.objects.order_by('-id')[0]
605 term = ApprovalTerms.objects.get(id=term_id)
606 except ApprovalTerms.DoesNotExist, e:
610 messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
611 return HttpResponseRedirect(reverse('index'))
612 f = open(term.location, 'r')
615 if request.method == 'POST':
616 next = restrict_next(
617 request.POST.get('next'),
621 next = reverse('index')
622 form = SignApprovalTermsForm(request.POST, instance=request.user)
623 if not form.is_valid():
624 return render_response(template_name,
626 approval_terms_form=form,
627 context_instance=get_context(request, extra_context))
629 return HttpResponseRedirect(next)
632 if request.user.is_authenticated() and not request.user.signed_terms:
633 form = SignApprovalTermsForm(instance=request.user)
634 return render_response(template_name,
636 approval_terms_form=form,
637 context_instance=get_context(request, extra_context))
640 @require_http_methods(["GET", "POST"])
642 @signed_terms_required
643 @transaction.commit_manually
644 def change_email(request, activation_key=None,
645 email_template_name='registration/email_change_email.txt',
646 form_template_name='registration/email_change_form.html',
647 confirm_template_name='registration/email_change_done.html',
649 extra_context = extra_context or {}
652 user = EmailChange.objects.change_email(activation_key)
653 if request.user.is_authenticated() and request.user == user:
654 msg = _(astakos_messages.EMAIL_CHANGED)
655 messages.success(request, msg)
657 response = prepare_response(request, user)
660 except ValueError, e:
661 messages.error(request, e)
662 return render_response(confirm_template_name,
663 modified_user=user if 'user' in locals(
665 context_instance=get_context(request,
668 if not request.user.is_authenticated():
669 path = quote(request.get_full_path())
670 url = request.build_absolute_uri(reverse('index'))
671 return HttpResponseRedirect(url + '?next=' + path)
672 form = EmailChangeForm(request.POST or None)
673 if request.method == 'POST' and form.is_valid():
675 ec = form.save(email_template_name, request)
676 except SendMailError, e:
678 messages.error(request, msg)
679 transaction.rollback()
680 except IntegrityError, e:
681 msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
682 messages.error(request, msg)
684 msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
685 messages.success(request, msg)
687 return render_response(
690 context_instance=get_context(request, extra_context)
694 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
696 if settings.MODERATION_ENABLED:
697 raise PermissionDenied
699 extra_context = extra_context or {}
701 u = AstakosUser.objects.get(id=user_id)
702 except AstakosUser.DoesNotExist:
703 messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
706 send_activation_func(u)
707 msg = _(astakos_messages.ACTIVATION_SENT)
708 messages.success(request, msg)
709 except SendMailError, e:
710 messages.error(request, e)
711 return render_response(
713 login_form = LoginForm(request=request),
714 context_instance = get_context(
720 class ResourcePresentation():
722 def __init__(self, data):
725 def update_from_result(self, result):
726 if result.is_success:
727 for r in result.data:
728 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
729 if not rname in self.data['resources']:
730 self.data['resources'][rname] = {}
732 self.data['resources'][rname].update(r)
733 self.data['resources'][rname]['id'] = rname
734 group = r.get('group')
735 if not group in self.data['groups']:
736 self.data['groups'][group] = {}
738 self.data['groups'][r.get('group')].update({'name': r.get('group')})
740 def test(self, quota_dict):
741 for k, v in quota_dict.iteritems():
744 if not rname in self.data['resources']:
745 self.data['resources'][rname] = {}
748 self.data['resources'][rname]['value'] = value
751 def update_from_result_report(self, result):
752 if result.is_success:
753 for r in result.data:
754 rname = r.get('name')
755 if not rname in self.data['resources']:
756 self.data['resources'][rname] = {}
758 self.data['resources'][rname].update(r)
759 self.data['resources'][rname]['id'] = rname
760 group = r.get('group')
761 if not group in self.data['groups']:
762 self.data['groups'][group] = {}
764 self.data['groups'][r.get('group')].update({'name': r.get('group')})
766 def get_group_resources(self, group):
767 return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
769 def get_groups_resources(self):
770 for g in self.data['groups']:
771 yield g, self.get_group_resources(g)
773 def get_quota(self, group_quotas):
774 for r, v in group_quotas.iteritems():
776 quota = self.data['resources'].get(rname)
781 def get_policies(self, policies_data):
782 for policy in policies_data:
783 rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
784 policy.update(self.data['resources'].get(rname))
788 return self.data.__repr__()
790 def __iter__(self, *args, **kwargs):
791 return self.data.__iter__(*args, **kwargs)
793 def __getitem__(self, *args, **kwargs):
794 return self.data.__getitem__(*args, **kwargs)
796 def get(self, *args, **kwargs):
797 return self.data.get(*args, **kwargs)
801 @require_http_methods(["GET", "POST"])
802 @signed_terms_required
804 def group_add(request, kind_name='default'):
806 result = callpoint.list_resources()
807 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
808 resource_catalog.update_from_result(result)
810 if not result.is_success:
813 'Unable to retrieve system resources: %s' % result.reason
817 kind = GroupKind.objects.get(name=kind_name)
819 return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
823 post_save_redirect = '/im/group/%(id)s/'
824 context_processors = None
825 model, form_class = get_model_and_form_class(
827 form_class=AstakosGroupCreationForm
830 if request.method == 'POST':
831 form = form_class(request.POST, request.FILES)
833 policies = form.policies()
834 return render_response(
835 template='im/astakosgroup_form_summary.html',
836 context_instance=get_context(request),
837 form=AstakosGroupCreationSummaryForm(form.cleaned_data),
838 policies=resource_catalog.get_policies(policies)
845 for group, resources in resource_catalog.get_groups_resources():
846 data['is_selected_%s' % group] = False
847 for resource in resources:
848 data['%s_uplimit' % resource] = ''
850 form = form_class(data)
852 # Create the template, context, response
853 template_name = "%s/%s_form.html" % (
854 model._meta.app_label,
855 model._meta.object_name.lower()
857 t = template_loader.get_template(template_name)
858 c = RequestContext(request, {
861 'resource_catalog':resource_catalog,
862 }, context_processors)
863 return HttpResponse(t.render(c))
866 #@require_http_methods(["POST"])
867 @require_http_methods(["GET", "POST"])
868 @signed_terms_required
870 def group_add_complete(request):
872 form = AstakosGroupCreationSummaryForm(request.POST)
874 d = form.cleaned_data
875 d['owners'] = [request.user]
876 result = callpoint.create_groups((d,)).next()
877 if result.is_success:
878 new_object = result.data[0]
879 msg = _(astakos_messages.OBJECT_CREATED) %\
880 {"verbose_name": model._meta.verbose_name}
881 messages.success(request, msg, fail_silently=True)
885 send_group_creation_notification(
886 template_name='im/group_creation_notification.txt',
889 'owner': request.user,
890 'policies': d.get('policies', [])
893 except SendNotificationError, e:
894 messages.error(request, e, fail_silently=True)
895 post_save_redirect = '/im/group/%(id)s/'
896 return HttpResponseRedirect(post_save_redirect % new_object)
898 d = {"verbose_name": model._meta.verbose_name,
899 "reason":result.reason}
900 msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
901 messages.error(request, msg, fail_silently=True)
902 return render_response(
903 template='im/astakosgroup_form_summary.html',
904 context_instance=get_context(request),
906 policies=form.cleaned_data.get('policies')
910 #@require_http_methods(["GET"])
911 @require_http_methods(["GET", "POST"])
912 @signed_terms_required
914 def group_list(request):
915 none = request.user.astakos_groups.none()
917 SELECT auth_group.id,
918 auth_group.name AS groupname,
919 im_groupkind.name AS kindname,
921 owner.email AS groupowner,
922 (SELECT COUNT(*) FROM im_membership
923 WHERE group_id = im_astakosgroup.group_ptr_id
924 AND date_joined IS NOT NULL) AS approved_members_num,
926 SELECT date_joined FROM im_membership
927 WHERE group_id = im_astakosgroup.group_ptr_id
928 AND person_id = %(userid)s) IS NULL
929 THEN 0 ELSE 1 END) AS membership_status
931 INNER JOIN im_membership ON (
932 im_astakosgroup.group_ptr_id = im_membership.group_id)
933 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
934 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
935 LEFT JOIN im_astakosuser_owner ON (
936 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
937 LEFT JOIN auth_user as owner ON (
938 im_astakosuser_owner.astakosuser_id = owner.id)
939 WHERE im_membership.person_id = %(userid)s
941 params = {'userid':request.user.id}
944 sorting = 'groupname'
945 sort_form = AstakosGroupSortForm(request.GET)
946 if sort_form.is_valid():
947 sorting = sort_form.cleaned_data.get('sorting')
948 query = query+" ORDER BY %s ASC" %sorting
950 q = AstakosGroup.objects.raw(query, params=params)
952 # Create the template, context, response
953 template_name = "%s/%s_list.html" % (
954 q.model._meta.app_label,
955 q.model._meta.object_name.lower()
957 extra_context = dict(
961 page=request.GET.get('page', 1)
963 return render_response(template_name,
964 context_instance=get_context(request, extra_context)
968 @require_http_methods(["GET", "POST"])
969 @signed_terms_required
971 def group_detail(request, group_id):
972 q = AstakosGroup.objects.select_related().filter(pk=group_id)
974 'is_member': """SELECT CASE WHEN EXISTS(
975 SELECT id FROM im_membership
976 WHERE group_id = im_astakosgroup.group_ptr_id
978 THEN 1 ELSE 0 END""" % request.user.id,
979 'is_owner': """SELECT CASE WHEN EXISTS(
980 SELECT id FROM im_astakosuser_owner
981 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
982 AND astakosuser_id = %s)
983 THEN 1 ELSE 0 END""" % request.user.id,
984 'kindname': """SELECT name FROM im_groupkind
985 WHERE id = im_astakosgroup.kind_id"""})
988 context_processors = None
992 except AstakosGroup.DoesNotExist:
993 raise Http404("No %s found matching the query" % (
994 model._meta.verbose_name))
996 update_form = AstakosGroupUpdateForm(instance=obj)
997 addmembers_form = AddGroupMembersForm()
998 if request.method == 'POST':
1000 addmembers_data = {}
1001 for k, v in request.POST.iteritems():
1002 if k in update_form.fields:
1004 if k in addmembers_form.fields:
1005 addmembers_data[k] = v
1006 update_data = update_data or None
1007 addmembers_data = addmembers_data or None
1008 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1009 addmembers_form = AddGroupMembersForm(addmembers_data)
1010 if update_form.is_valid():
1012 if addmembers_form.is_valid():
1014 map(obj.approve_member, addmembers_form.valid_users)
1015 except AssertionError:
1016 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1017 messages.error(request, msg)
1018 addmembers_form = AddGroupMembersForm()
1020 template_name = "%s/%s_detail.html" % (
1021 model._meta.app_label, model._meta.object_name.lower())
1022 t = template_loader.get_template(template_name)
1023 c = RequestContext(request, {
1025 }, context_processors)
1028 sorting = 'person__email'
1029 form = MembersSortForm(request.GET)
1031 sorting = form.cleaned_data.get('sorting')
1033 result = callpoint.list_resources()
1034 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1035 resource_catalog.update_from_result(result)
1038 if not result.is_success:
1041 'Unable to retrieve system resources: %s' % result.reason
1044 extra_context = {'update_form': update_form,
1045 'addmembers_form': addmembers_form,
1046 'page': request.GET.get('page', 1),
1048 'resource_catalog':resource_catalog,
1049 'quota':resource_catalog.get_quota(obj.quota)}
1050 for key, value in extra_context.items():
1055 response = HttpResponse(t.render(c), mimetype=mimetype)
1057 request, response, model, getattr(obj, obj._meta.pk.name))
1061 @require_http_methods(["GET", "POST"])
1062 @signed_terms_required
1064 def group_search(request, extra_context=None, **kwargs):
1065 q = request.GET.get('q')
1066 if request.method == 'GET':
1067 form = AstakosGroupSearchForm({'q': q} if q else None)
1069 form = AstakosGroupSearchForm(get_query(request))
1071 q = form.cleaned_data['q'].strip()
1073 sorting = 'groupname'
1075 queryset = AstakosGroup.objects.select_related()
1076 queryset = queryset.filter(name__contains=q)
1077 queryset = queryset.filter(approval_date__isnull=False)
1078 queryset = queryset.extra(select={
1079 'groupname': "auth_group.name",
1080 'kindname': "im_groupkind.name",
1081 'approved_members_num': """
1082 SELECT COUNT(*) FROM im_membership
1083 WHERE group_id = im_astakosgroup.group_ptr_id
1084 AND date_joined IS NOT NULL""",
1085 'membership_approval_date': """
1086 SELECT date_joined FROM im_membership
1087 WHERE group_id = im_astakosgroup.group_ptr_id
1088 AND person_id = %s""" % request.user.id,
1090 SELECT CASE WHEN EXISTS(
1091 SELECT date_joined FROM im_membership
1092 WHERE group_id = im_astakosgroup.group_ptr_id
1094 THEN 1 ELSE 0 END""" % request.user.id,
1096 SELECT CASE WHEN EXISTS(
1097 SELECT id FROM im_astakosuser_owner
1098 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1099 AND astakosuser_id = %s)
1100 THEN 1 ELSE 0 END""" % request.user.id,
1101 'is_owner': """SELECT CASE WHEN EXISTS(
1102 SELECT id FROM im_astakosuser_owner
1103 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1104 AND astakosuser_id = %s)
1105 THEN 1 ELSE 0 END""" % request.user.id,
1109 sort_form = AstakosGroupSortForm(request.GET)
1110 if sort_form.is_valid():
1111 sorting = sort_form.cleaned_data.get('sorting')
1112 queryset = queryset.order_by(sorting)
1115 queryset = AstakosGroup.objects.none()
1119 paginate_by=PAGINATE_BY_ALL,
1120 page=request.GET.get('page') or 1,
1121 template_name='im/astakosgroup_list.html',
1122 extra_context=dict(form=form,
1128 @require_http_methods(["GET", "POST"])
1129 @signed_terms_required
1131 def group_all(request, extra_context=None, **kwargs):
1132 q = AstakosGroup.objects.select_related()
1133 q = q.filter(approval_date__isnull=False)
1134 q = q.extra(select={
1135 'groupname': "auth_group.name",
1136 'kindname': "im_groupkind.name",
1137 'approved_members_num': """
1138 SELECT COUNT(*) FROM im_membership
1139 WHERE group_id = im_astakosgroup.group_ptr_id
1140 AND date_joined IS NOT NULL""",
1141 'membership_approval_date': """
1142 SELECT date_joined FROM im_membership
1143 WHERE group_id = im_astakosgroup.group_ptr_id
1144 AND person_id = %s""" % request.user.id,
1146 SELECT CASE WHEN EXISTS(
1147 SELECT date_joined FROM im_membership
1148 WHERE group_id = im_astakosgroup.group_ptr_id
1150 THEN 1 ELSE 0 END""" % request.user.id,
1151 'is_owner': """SELECT CASE WHEN EXISTS(
1152 SELECT id FROM im_astakosuser_owner
1153 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1154 AND astakosuser_id = %s)
1155 THEN 1 ELSE 0 END""" % request.user.id, })
1158 sorting = 'groupname'
1159 print '>>>', sorting, request.GET
1160 sort_form = AstakosGroupSortForm(request.GET)
1161 if sort_form.is_valid():
1162 sorting = sort_form.cleaned_data.get('sorting')
1163 print '<<<', sorting
1164 q = q.order_by(sorting)
1169 paginate_by=PAGINATE_BY_ALL,
1170 page=request.GET.get('page') or 1,
1171 template_name='im/astakosgroup_list.html',
1172 extra_context=dict(form=AstakosGroupSearchForm(),
1177 #@require_http_methods(["POST"])
1178 @require_http_methods(["POST", "GET"])
1179 @signed_terms_required
1181 def group_join(request, group_id):
1182 m = Membership(group_id=group_id,
1183 person=request.user,
1184 date_requested=datetime.now())
1187 post_save_redirect = reverse(
1189 kwargs=dict(group_id=group_id))
1190 return HttpResponseRedirect(post_save_redirect)
1191 except IntegrityError, e:
1193 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1194 messages.error(request, msg)
1195 return group_search(request)
1198 @require_http_methods(["POST"])
1199 @signed_terms_required
1201 def group_leave(request, group_id):
1203 m = Membership.objects.select_related().get(
1205 person=request.user)
1206 except Membership.DoesNotExist:
1207 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1208 if request.user in m.group.owner.all():
1209 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1210 return delete_object(
1214 template_name='im/astakosgroup_list.html',
1215 post_delete_redirect=reverse(
1217 kwargs=dict(group_id=group_id)))
1220 def handle_membership(func):
1222 def wrapper(request, group_id, user_id):
1224 m = Membership.objects.select_related().get(
1227 except Membership.DoesNotExist:
1228 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1230 if request.user not in m.group.owner.all():
1231 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1233 return group_detail(request, group_id)
1237 #@require_http_methods(["POST"])
1238 @require_http_methods(["POST", "GET"])
1239 @signed_terms_required
1242 def approve_member(request, membership):
1244 membership.approve()
1245 realname = membership.person.realname
1246 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1247 messages.success(request, msg)
1248 except AssertionError:
1249 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1250 messages.error(request, msg)
1251 except BaseException, e:
1253 realname = membership.person.realname
1254 msg = _(astakos_messages.GENERIC_ERROR)
1255 messages.error(request, msg)
1258 @signed_terms_required
1261 def disapprove_member(request, membership):
1263 membership.disapprove()
1264 realname = membership.person.realname
1265 msg = astakos_messages.MEMBER_REMOVED % locals()
1266 messages.success(request, msg)
1267 except BaseException, e:
1269 msg = _(astakos_messages.GENERIC_ERROR)
1270 messages.error(request, msg)
1273 #@require_http_methods(["GET"])
1274 @require_http_methods(["POST", "GET"])
1275 @signed_terms_required
1277 def resource_list(request):
1278 def with_class(entry):
1279 entry['load_class'] = 'red'
1280 max_value = float(entry['maxValue'])
1281 curr_value = float(entry['currValue'])
1283 entry['ratio'] = (curr_value / max_value) * 100
1286 if entry['ratio'] < 66:
1287 entry['load_class'] = 'yellow'
1288 if entry['ratio'] < 33:
1289 entry['load_class'] = 'green'
1292 def pluralize(entry):
1293 entry['plural'] = engine.plural(entry.get('name'))
1296 result = callpoint.get_user_status(request.user.id)
1297 if result.is_success:
1298 backenddata = map(with_class, result.data)
1299 data = map(pluralize, result.data)
1302 messages.error(request, result.reason)
1303 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1304 resource_catalog.update_from_result_report(result)
1308 return render_response('im/resource_list.html',
1310 context_instance=get_context(request),
1311 resource_catalog=resource_catalog,
1315 def group_create_list(request):
1316 form = PickResourceForm()
1317 return render_response(
1318 template='im/astakosgroup_create_list.html',
1319 context_instance=get_context(request),)
1322 #@require_http_methods(["GET"])
1323 @require_http_methods(["POST", "GET"])
1324 @signed_terms_required
1326 def billing(request):
1328 today = datetime.today()
1329 month_last_day = calendar.monthrange(today.year, today.month)[1]
1330 start = request.POST.get('datefrom', None)
1332 today = datetime.fromtimestamp(int(start))
1333 month_last_day = calendar.monthrange(today.year, today.month)[1]
1335 start = datetime(today.year, today.month, 1).strftime("%s")
1336 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1337 r = request_billing.apply(args=('pgerakios@grnet.gr',
1343 status, data = r.result
1344 data = _clear_billing_data(data)
1346 messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1348 messages.error(request, r.result)
1350 return render_response(
1351 template='im/billing.html',
1352 context_instance=get_context(request),
1354 zerodate=datetime(month=1, year=1970, day=1),
1357 month_last_day=month_last_day)
1360 def _clear_billing_data(data):
1362 # remove addcredits entries
1364 return e['serviceName'] != "addcredits"
1367 def servicefilter(service_name):
1368 service = service_name
1371 return e['serviceName'] == service
1374 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1375 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1376 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1377 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1382 #@require_http_methods(["GET"])
1383 @require_http_methods(["POST", "GET"])
1384 @signed_terms_required
1386 def timeline(request):
1387 # data = {'entity':request.user.email}
1389 timeline_header = ()
1390 # form = TimelineForm(data)
1391 form = TimelineForm()
1392 if request.method == 'POST':
1394 form = TimelineForm(data)
1396 data = form.cleaned_data
1397 timeline_header = ('entity', 'resource',
1398 'event name', 'event date',
1399 'incremental cost', 'total cost')
1400 timeline_body = timeline_charge(
1401 data['entity'], data['resource'],
1402 data['start_date'], data['end_date'],
1403 data['details'], data['operation'])
1405 return render_response(template='im/timeline.html',
1406 context_instance=get_context(request),
1408 timeline_header=timeline_header,
1409 timeline_body=timeline_body)
1412 # TODO: action only on POST and user should confirm the removal
1413 @require_http_methods(["GET", "POST"])
1415 @signed_terms_required
1416 def remove_auth_provider(request, pk):
1418 provider = request.user.auth_providers.get(pk=pk)
1419 except AstakosUserAuthProvider.DoesNotExist:
1422 if provider.can_remove():
1424 return HttpResponseRedirect(reverse('edit_profile'))
1426 raise PermissionDenied