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.shortcuts import get_object_or_404
45 from django.contrib import messages
46 from django.contrib.auth.decorators import login_required
47 from django.core.urlresolvers import reverse
48 from django.db import transaction
49 from django.db.utils import IntegrityError
50 from django.http import (HttpResponse, HttpResponseBadRequest,
51 HttpResponseForbidden, HttpResponseRedirect,
52 HttpResponseBadRequest, Http404)
53 from django.shortcuts import redirect
54 from django.template import RequestContext, loader as template_loader
55 from django.utils.http import urlencode
56 from django.utils.translation import ugettext as _
57 from django.views.generic.create_update import (delete_object,
58 get_model_and_form_class)
59 from django.views.generic.list_detail import object_list
60 from django.core.xheaders import populate_xheaders
61 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 django.db.models import Q
66 import astakos.im.messages as astakos_messages
68 from astakos.im.activation_backends import get_backend, SimpleBackend
69 from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
70 EmailChange, GroupKind, Membership,
71 RESOURCE_SEPARATOR, AstakosUserAuthProvider,
72 PendingThirdPartyUser)
73 from astakos.im.util import get_context, prepare_response, get_query, restrict_next
74 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
75 FeedbackForm, SignApprovalTermsForm,
77 AstakosGroupCreationForm, AstakosGroupSearchForm,
78 AstakosGroupUpdateForm, AddGroupMembersForm,
79 MembersSortForm, AstakosGroupSortForm,
80 TimelineForm, PickResourceForm,
81 AstakosGroupCreationSummaryForm)
82 from astakos.im.functions import (send_feedback, SendMailError,
83 logout as auth_logout,
84 activate as activate_func,
85 send_activation as send_activation_func,
86 send_group_creation_notification,
87 SendNotificationError)
88 from astakos.im.endpoints.qh import timeline_charge
89 from astakos.im.settings import (COOKIE_DOMAIN, LOGOUT_NEXT,
90 LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
91 #from astakos.im.tasks import request_billing
92 from astakos.im.api.callpoint import AstakosCallpoint
94 from astakos.im import settings
95 from astakos.im import auth_providers
97 logger = logging.getLogger(__name__)
99 callpoint = AstakosCallpoint()
101 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
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``.
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)
115 def requires_auth_provider(provider_id, **perms):
118 def decorator(func, *args, **kwargs):
120 def wrapper(request, *args, **kwargs):
121 provider = auth_providers.get_provider(provider_id)
123 if not provider or not provider.is_active():
124 raise PermissionDenied
127 for pkey, value in perms.iteritems():
128 attr = 'is_available_for_%s' % pkey.lower()
129 if getattr(provider, attr)() != value:
130 #TODO: add session message
131 return HttpResponseRedirect(reverse('login'))
132 return func(request, *args)
137 def requires_anonymous(func):
139 Decorator checkes whether the request.user is not Anonymous and in that case
140 redirects to `logout`.
143 def wrapper(request, *args):
144 if not request.user.is_anonymous():
145 next = urlencode({'next': request.build_absolute_uri()})
146 logout_uri = reverse(logout) + '?' + next
147 return HttpResponseRedirect(logout_uri)
148 return func(request, *args)
152 def signed_terms_required(func):
154 Decorator checks whether the request.user is Anonymous and in that case
155 redirects to `logout`.
158 def wrapper(request, *args, **kwargs):
159 if request.user.is_authenticated() and not request.user.signed_terms:
160 params = urlencode({'next': request.build_absolute_uri(),
162 terms_uri = reverse('latest_terms') + '?' + params
163 return HttpResponseRedirect(terms_uri)
164 return func(request, *args, **kwargs)
168 def required_auth_methods_assigned(only_warn=False):
170 Decorator that checks whether the request.user has all required auth providers
173 required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
176 if not required_providers:
180 def wrapper(request, *args, **kwargs):
181 if request.user.is_authenticated():
182 for required in required_providers:
183 if not request.user.has_auth_provider(required):
184 provider = auth_providers.get_provider(required)
186 messages.error(request,
187 _(astakos_messages.AUTH_PROVIDER_REQUIRED % {
188 'provider': provider.get_title_display}))
190 return HttpResponseRedirect(reverse('edit_profile'))
191 return func(request, *args, **kwargs)
196 def valid_astakos_user_required(func):
197 return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
200 @require_http_methods(["GET", "POST"])
201 @signed_terms_required
202 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
204 If there is logged on user renders the profile page otherwise renders login page.
208 ``login_template_name``
209 A custom login template to use. This is optional; if not specified,
210 this will default to ``im/login.html``.
212 ``profile_template_name``
213 A custom profile template to use. This is optional; if not specified,
214 this will default to ``im/profile.html``.
217 An dictionary of variables to add to the template context.
221 im/profile.html or im/login.html or ``template_name`` keyword argument.
224 extra_context = extra_context or {}
225 template_name = login_template_name
226 if request.user.is_authenticated():
227 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
229 return render_response(
231 login_form = LoginForm(request=request),
232 context_instance = get_context(request, extra_context)
236 @require_http_methods(["GET", "POST"])
237 @valid_astakos_user_required
238 @transaction.commit_manually
239 def invite(request, template_name='im/invitations.html', extra_context=None):
241 Allows a user to invite somebody else.
243 In case of GET request renders a form for providing the invitee information.
244 In case of POST checks whether the user has not run out of invitations and then
245 sends an invitation email to singup to the service.
247 The view uses commit_manually decorator in order to ensure the number of the
248 user invitations is going to be updated only if the email has been successfully sent.
250 If the user isn't logged in, redirects to settings.LOGIN_URL.
255 A custom template to use. This is optional; if not specified,
256 this will default to ``im/invitations.html``.
259 An dictionary of variables to add to the template context.
263 im/invitations.html or ``template_name`` keyword argument.
267 The view expectes the following settings are defined:
269 * LOGIN_URL: login uri
271 extra_context = extra_context or {}
274 form = InvitationForm()
276 inviter = request.user
277 if request.method == 'POST':
278 form = InvitationForm(request.POST)
279 if inviter.invitations > 0:
282 email = form.cleaned_data.get('username')
283 realname = form.cleaned_data.get('realname')
284 inviter.invite(email, realname)
285 message = _(astakos_messages.INVITATION_SENT) % locals()
286 messages.success(request, message)
287 except SendMailError, e:
289 messages.error(request, message)
290 transaction.rollback()
291 except BaseException, e:
292 message = _(astakos_messages.GENERIC_ERROR)
293 messages.error(request, message)
295 transaction.rollback()
299 message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
300 messages.error(request, message)
302 sent = [{'email': inv.username,
303 'realname': inv.realname,
304 'is_consumed': inv.is_consumed}
305 for inv in request.user.invitations_sent.all()]
306 kwargs = {'inviter': inviter,
308 context = get_context(request, extra_context, **kwargs)
309 return render_response(template_name,
310 invitation_form=form,
311 context_instance=context)
314 @require_http_methods(["GET", "POST"])
315 @required_auth_methods_assigned(only_warn=True)
317 @signed_terms_required
318 def edit_profile(request, template_name='im/profile.html', extra_context=None):
320 Allows a user to edit his/her profile.
322 In case of GET request renders a form for displaying the user information.
323 In case of POST updates the user informantion and redirects to ``next``
324 url parameter if exists.
326 If the user isn't logged in, redirects to settings.LOGIN_URL.
331 A custom template to use. This is optional; if not specified,
332 this will default to ``im/profile.html``.
335 An dictionary of variables to add to the template context.
339 im/profile.html or ``template_name`` keyword argument.
343 The view expectes the following settings are defined:
345 * LOGIN_URL: login uri
347 extra_context = extra_context or {}
349 instance=request.user,
350 session_key=request.session.session_key
352 extra_context['next'] = request.GET.get('next')
353 if request.method == 'POST':
356 instance=request.user,
357 session_key=request.session.session_key
361 prev_token = request.user.auth_token
365 session_key=request.session.session_key
367 next = restrict_next(
368 request.POST.get('next'),
372 return redirect(next)
373 msg = _(astakos_messages.PROFILE_UPDATED)
374 messages.success(request, msg)
375 except ValueError, ve:
376 messages.success(request, ve)
377 elif request.method == "GET":
378 request.user.is_verified = True
382 user_providers = request.user.get_active_auth_providers()
384 # providers that user can add
385 user_available_providers = request.user.get_available_auth_providers()
387 return render_response(template_name,
389 user_providers = user_providers,
390 user_available_providers = user_available_providers,
391 context_instance = get_context(request,
395 @transaction.commit_manually
396 @require_http_methods(["GET", "POST"])
397 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
399 Allows a user to create a local account.
401 In case of GET request renders a form for entering the user information.
402 In case of POST handles the signup.
404 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
405 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
406 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
407 (see activation_backends);
409 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
410 otherwise renders the same page with a success message.
412 On unsuccessful creation, renders ``template_name`` with an error message.
417 A custom template to render. This is optional;
418 if not specified, this will default to ``im/signup.html``.
421 A custom template to render in case of success. This is optional;
422 if not specified, this will default to ``im/signup_complete.html``.
425 An dictionary of variables to add to the template context.
429 im/signup.html or ``template_name`` keyword argument.
430 im/signup_complete.html or ``on_success`` keyword argument.
432 extra_context = extra_context or {}
433 if request.user.is_authenticated():
434 return HttpResponseRedirect(reverse('edit_profile'))
436 provider = get_query(request).get('provider', 'local')
437 if not auth_providers.get_provider(provider).is_available_for_create():
438 raise PermissionDenied
440 id = get_query(request).get('id')
442 instance = AstakosUser.objects.get(id=id) if id else None
443 except AstakosUser.DoesNotExist:
446 third_party_token = request.REQUEST.get('third_party_token', None)
447 if third_party_token:
448 pending = get_object_or_404(PendingThirdPartyUser,
449 token=third_party_token)
450 provider = pending.provider
451 instance = pending.get_user_instance()
455 backend = get_backend(request)
456 form = backend.get_signup_form(provider, instance)
458 form = SimpleBackend(request).get_signup_form(provider)
459 messages.error(request, e)
460 if request.method == 'POST':
462 user = form.save(commit=False)
464 result = backend.handle_activation(user)
465 status = messages.SUCCESS
466 message = result.message
468 form.store_user(user, request)
470 if 'additional_email' in form.cleaned_data:
471 additional_email = form.cleaned_data['additional_email']
472 if additional_email != user.email:
473 user.additionalmail_set.create(email=additional_email)
474 msg = 'Additional email: %s saved for user %s.' % (
478 logger._log(LOGGING_LEVEL, msg, [])
479 if user and user.is_active:
480 next = request.POST.get('next', '')
481 response = prepare_response(request, user, next=next)
484 messages.add_message(request, status, message)
486 return render_response(
488 context_instance=get_context(
493 except SendMailError, e:
495 status = messages.ERROR
497 messages.error(request, message)
498 transaction.rollback()
499 except BaseException, e:
501 message = _(astakos_messages.GENERIC_ERROR)
502 messages.error(request, message)
504 transaction.rollback()
505 return render_response(template_name,
507 third_party_token=third_party_token,
509 context_instance=get_context(request, extra_context))
512 @require_http_methods(["GET", "POST"])
513 @required_auth_methods_assigned(only_warn=True)
515 @signed_terms_required
516 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
518 Allows a user to send feedback.
520 In case of GET request renders a form for providing the feedback information.
521 In case of POST sends an email to support team.
523 If the user isn't logged in, redirects to settings.LOGIN_URL.
528 A custom template to use. This is optional; if not specified,
529 this will default to ``im/feedback.html``.
532 An dictionary of variables to add to the template context.
536 im/signup.html or ``template_name`` keyword argument.
540 * LOGIN_URL: login uri
542 extra_context = extra_context or {}
543 if request.method == 'GET':
544 form = FeedbackForm()
545 if request.method == 'POST':
547 return HttpResponse('Unauthorized', status=401)
549 form = FeedbackForm(request.POST)
551 msg = form.cleaned_data['feedback_msg']
552 data = form.cleaned_data['feedback_data']
554 send_feedback(msg, data, request.user, email_template_name)
555 except SendMailError, e:
556 messages.error(request, message)
558 message = _(astakos_messages.FEEDBACK_SENT)
559 messages.success(request, message)
560 return render_response(template_name,
562 context_instance=get_context(request, extra_context))
565 @require_http_methods(["GET"])
566 @signed_terms_required
567 def logout(request, template='registration/logged_out.html', extra_context=None):
569 Wraps `django.contrib.auth.logout`.
571 extra_context = extra_context or {}
572 response = HttpResponse()
573 if request.user.is_authenticated():
574 email = request.user.email
577 response['Location'] = reverse('index')
578 response.status_code = 301
581 next = restrict_next(
582 request.GET.get('next'),
587 response['Location'] = next
588 response.status_code = 302
590 response['Location'] = LOGOUT_NEXT
591 response.status_code = 301
593 messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
594 response['Location'] = reverse('index')
595 response.status_code = 301
599 @require_http_methods(["GET", "POST"])
600 @transaction.commit_manually
601 def activate(request, greeting_email_template_name='im/welcome_email.txt',
602 helpdesk_email_template_name='im/helpdesk_notification.txt'):
604 Activates the user identified by the ``auth`` request parameter, sends a welcome email
605 and renews the user token.
607 The view uses commit_manually decorator in order to ensure the user state will be updated
608 only if the email will be send successfully.
610 token = request.GET.get('auth')
611 next = request.GET.get('next')
613 user = AstakosUser.objects.get(auth_token=token)
614 except AstakosUser.DoesNotExist:
615 return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
618 message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
619 messages.error(request, message)
620 return index(request)
623 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
624 response = prepare_response(request, user, next, renew=True)
627 except SendMailError, e:
629 messages.add_message(request, messages.ERROR, message)
630 transaction.rollback()
631 return index(request)
632 except BaseException, e:
633 status = messages.ERROR
634 message = _(astakos_messages.GENERIC_ERROR)
635 messages.add_message(request, messages.ERROR, message)
637 transaction.rollback()
638 return index(request)
641 @require_http_methods(["GET", "POST"])
642 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
643 extra_context = extra_context or {}
648 term = ApprovalTerms.objects.order_by('-id')[0]
653 term = ApprovalTerms.objects.get(id=term_id)
654 except ApprovalTerms.DoesNotExist, e:
658 messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
659 return HttpResponseRedirect(reverse('index'))
660 f = open(term.location, 'r')
663 if request.method == 'POST':
664 next = restrict_next(
665 request.POST.get('next'),
669 next = reverse('index')
670 form = SignApprovalTermsForm(request.POST, instance=request.user)
671 if not form.is_valid():
672 return render_response(template_name,
674 approval_terms_form=form,
675 context_instance=get_context(request, extra_context))
677 return HttpResponseRedirect(next)
680 if request.user.is_authenticated() and not request.user.signed_terms:
681 form = SignApprovalTermsForm(instance=request.user)
682 return render_response(template_name,
684 approval_terms_form=form,
685 context_instance=get_context(request, extra_context))
688 @require_http_methods(["GET", "POST"])
689 @valid_astakos_user_required
690 @transaction.commit_manually
691 def change_email(request, activation_key=None,
692 email_template_name='registration/email_change_email.txt',
693 form_template_name='registration/email_change_form.html',
694 confirm_template_name='registration/email_change_done.html',
696 extra_context = extra_context or {}
699 user = EmailChange.objects.change_email(activation_key)
700 if request.user.is_authenticated() and request.user == user:
701 msg = _(astakos_messages.EMAIL_CHANGED)
702 messages.success(request, msg)
704 response = prepare_response(request, user)
707 except ValueError, e:
708 messages.error(request, e)
709 return render_response(confirm_template_name,
710 modified_user=user if 'user' in locals(
712 context_instance=get_context(request,
715 if not request.user.is_authenticated():
716 path = quote(request.get_full_path())
717 url = request.build_absolute_uri(reverse('index'))
718 return HttpResponseRedirect(url + '?next=' + path)
719 form = EmailChangeForm(request.POST or None)
720 if request.method == 'POST' and form.is_valid():
722 ec = form.save(email_template_name, request)
723 except SendMailError, e:
725 messages.error(request, msg)
726 transaction.rollback()
727 except IntegrityError, e:
728 msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
729 messages.error(request, msg)
731 msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
732 messages.success(request, msg)
734 return render_response(
737 context_instance=get_context(request, extra_context)
741 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
743 if request.user.is_authenticated():
744 messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
745 return HttpResponseRedirect(reverse('edit_profile'))
747 if settings.MODERATION_ENABLED:
748 raise PermissionDenied
750 extra_context = extra_context or {}
752 u = AstakosUser.objects.get(id=user_id)
753 except AstakosUser.DoesNotExist:
754 messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
757 send_activation_func(u)
758 msg = _(astakos_messages.ACTIVATION_SENT)
759 messages.success(request, msg)
760 except SendMailError, e:
761 messages.error(request, e)
762 return render_response(
764 login_form = LoginForm(request=request),
765 context_instance = get_context(
771 class ResourcePresentation():
773 def __init__(self, data):
776 def update_from_result(self, result):
777 if result.is_success:
778 for r in result.data:
779 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
780 if not rname in self.data['resources']:
781 self.data['resources'][rname] = {}
783 self.data['resources'][rname].update(r)
784 self.data['resources'][rname]['id'] = rname
785 group = r.get('group')
786 if not group in self.data['groups']:
787 self.data['groups'][group] = {}
789 self.data['groups'][r.get('group')].update({'name': r.get('group')})
791 def test(self, quota_dict):
792 for k, v in quota_dict.iteritems():
795 if not rname in self.data['resources']:
796 self.data['resources'][rname] = {}
799 self.data['resources'][rname]['value'] = value
802 def update_from_result_report(self, result):
803 if result.is_success:
804 for r in result.data:
805 rname = r.get('name')
806 if not rname in self.data['resources']:
807 self.data['resources'][rname] = {}
809 self.data['resources'][rname].update(r)
810 self.data['resources'][rname]['id'] = rname
811 group = r.get('group')
812 if not group in self.data['groups']:
813 self.data['groups'][group] = {}
815 self.data['groups'][r.get('group')].update({'name': r.get('group')})
817 def get_group_resources(self, group):
818 return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
820 def get_groups_resources(self):
821 for g in self.data['groups']:
822 yield g, self.get_group_resources(g)
824 def get_quota(self, group_quotas):
825 for r, v in group_quotas.iteritems():
827 quota = self.data['resources'].get(rname)
832 def get_policies(self, policies_data):
833 for policy in policies_data:
834 rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
835 policy.update(self.data['resources'].get(rname))
839 return self.data.__repr__()
841 def __iter__(self, *args, **kwargs):
842 return self.data.__iter__(*args, **kwargs)
844 def __getitem__(self, *args, **kwargs):
845 return self.data.__getitem__(*args, **kwargs)
847 def get(self, *args, **kwargs):
848 return self.data.get(*args, **kwargs)
852 @require_http_methods(["GET", "POST"])
853 @valid_astakos_user_required
854 def group_add(request, kind_name='default'):
856 result = callpoint.list_resources()
857 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
858 resource_catalog.update_from_result(result)
860 if not result.is_success:
863 'Unable to retrieve system resources: %s' % result.reason
867 kind = GroupKind.objects.get(name=kind_name)
869 return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
873 post_save_redirect = '/im/group/%(id)s/'
874 context_processors = None
875 model, form_class = get_model_and_form_class(
877 form_class=AstakosGroupCreationForm
880 if request.method == 'POST':
881 form = form_class(request.POST, request.FILES)
883 policies = form.policies()
884 return render_response(
885 template='im/astakosgroup_form_summary.html',
886 context_instance=get_context(request),
887 form=AstakosGroupCreationSummaryForm(form.cleaned_data),
888 policies=resource_catalog.get_policies(policies)
895 for group, resources in resource_catalog.get_groups_resources():
896 data['is_selected_%s' % group] = False
897 for resource in resources:
898 data['%s_uplimit' % resource] = ''
900 form = form_class(data)
902 # Create the template, context, response
903 template_name = "%s/%s_form.html" % (
904 model._meta.app_label,
905 model._meta.object_name.lower()
907 t = template_loader.get_template(template_name)
908 c = RequestContext(request, {
911 'resource_catalog':resource_catalog,
912 }, context_processors)
913 return HttpResponse(t.render(c))
916 #@require_http_methods(["POST"])
917 @require_http_methods(["GET", "POST"])
918 @valid_astakos_user_required
919 def group_add_complete(request):
921 form = AstakosGroupCreationSummaryForm(request.POST)
923 d = form.cleaned_data
924 d['owners'] = [request.user]
925 result = callpoint.create_groups((d,)).next()
926 if result.is_success:
927 new_object = result.data[0]
928 msg = _(astakos_messages.OBJECT_CREATED) %\
929 {"verbose_name": model._meta.verbose_name}
930 messages.success(request, msg, fail_silently=True)
934 send_group_creation_notification(
935 template_name='im/group_creation_notification.txt',
938 'owner': request.user,
939 'policies': d.get('policies', [])
942 except SendNotificationError, e:
943 messages.error(request, e, fail_silently=True)
944 post_save_redirect = '/im/group/%(id)s/'
945 return HttpResponseRedirect(post_save_redirect % new_object)
947 d = {"verbose_name": model._meta.verbose_name,
948 "reason":result.reason}
949 msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
950 messages.error(request, msg, fail_silently=True)
951 return render_response(
952 template='im/astakosgroup_form_summary.html',
953 context_instance=get_context(request),
955 policies=form.cleaned_data.get('policies')
959 #@require_http_methods(["GET"])
960 @require_http_methods(["GET", "POST"])
961 @valid_astakos_user_required
962 def group_list(request):
963 none = request.user.astakos_groups.none()
965 SELECT auth_group.id,
966 auth_group.name AS groupname,
967 im_groupkind.name AS kindname,
969 owner.email AS groupowner,
970 (SELECT COUNT(*) FROM im_membership
971 WHERE group_id = im_astakosgroup.group_ptr_id
972 AND date_joined IS NOT NULL) AS approved_members_num,
974 SELECT date_joined FROM im_membership
975 WHERE group_id = im_astakosgroup.group_ptr_id
976 AND person_id = %(id)s) IS NULL
977 THEN 0 ELSE 1 END) AS membership_status
979 INNER JOIN im_membership ON (
980 im_astakosgroup.group_ptr_id = im_membership.group_id)
981 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
982 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
983 LEFT JOIN im_astakosuser_owner ON (
984 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
985 LEFT JOIN auth_user as owner ON (
986 im_astakosuser_owner.astakosuser_id = owner.id)
987 WHERE im_membership.person_id = %(id)s
988 AND im_groupkind.name != 'default'
989 """ % request.user.__dict__
992 sorting = 'groupname'
994 sort_form = AstakosGroupSortForm(request.GET)
995 if sort_form.is_valid():
996 sorting = sort_form.cleaned_data.get('sorting')
997 ordering = request.GET.get('ordering', 'ASC' )
999 query = query+" ORDER BY %s %s" %(sorting, ordering)
1001 q = AstakosGroup.objects.raw(query)
1003 # Create the template, context, response
1004 template_name = "%s/%s_list.html" % (
1005 q.model._meta.app_label,
1006 q.model._meta.object_name.lower()
1009 extra_context = dict(
1013 page=request.GET.get('page', 1),
1016 return render_response(template_name,
1017 context_instance=get_context(request, extra_context)
1021 @require_http_methods(["GET", "POST"])
1022 @valid_astakos_user_required
1023 def group_detail(request, group_id):
1024 q = AstakosGroup.objects.select_related().filter(pk=group_id)
1025 q = q.extra(select={
1026 'is_member': """SELECT CASE WHEN EXISTS(
1027 SELECT id FROM im_membership
1028 WHERE group_id = im_astakosgroup.group_ptr_id
1030 THEN 1 ELSE 0 END""" % request.user.id,
1031 'is_owner': """SELECT CASE WHEN EXISTS(
1032 SELECT id FROM im_astakosuser_owner
1033 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1034 AND astakosuser_id = %s)
1035 THEN 1 ELSE 0 END""" % request.user.id,
1036 'is_active_member': """SELECT CASE WHEN(
1037 SELECT date_joined FROM im_membership
1038 WHERE group_id = im_astakosgroup.group_ptr_id
1039 AND person_id = %s) IS NULL
1040 THEN 0 ELSE 1 END""" % request.user.id,
1041 'kindname': """SELECT name FROM im_groupkind
1042 WHERE id = im_astakosgroup.kind_id"""})
1045 context_processors = None
1049 except AstakosGroup.DoesNotExist:
1050 raise Http404("No %s found matching the query" % (
1051 model._meta.verbose_name))
1053 update_form = AstakosGroupUpdateForm(instance=obj)
1054 addmembers_form = AddGroupMembersForm()
1055 if request.method == 'POST':
1057 addmembers_data = {}
1058 for k, v in request.POST.iteritems():
1059 if k in update_form.fields:
1061 if k in addmembers_form.fields:
1062 addmembers_data[k] = v
1063 update_data = update_data or None
1064 addmembers_data = addmembers_data or None
1065 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1066 addmembers_form = AddGroupMembersForm(addmembers_data)
1067 if update_form.is_valid():
1069 if addmembers_form.is_valid():
1071 map(obj.approve_member, addmembers_form.valid_users)
1072 except AssertionError:
1073 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1074 messages.error(request, msg)
1075 addmembers_form = AddGroupMembersForm()
1077 template_name = "%s/%s_detail.html" % (
1078 model._meta.app_label, model._meta.object_name.lower())
1079 t = template_loader.get_template(template_name)
1080 c = RequestContext(request, {
1082 }, context_processors)
1085 sorting = 'person__email'
1086 form = MembersSortForm(request.GET)
1088 sorting = form.cleaned_data.get('sorting')
1090 result = callpoint.list_resources()
1091 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1092 resource_catalog.update_from_result(result)
1095 if not result.is_success:
1098 'Unable to retrieve system resources: %s' % result.reason
1101 extra_context = {'update_form': update_form,
1102 'addmembers_form': addmembers_form,
1103 'page': request.GET.get('page', 1),
1105 'resource_catalog':resource_catalog,
1106 'quota':resource_catalog.get_quota(obj.quota)}
1107 for key, value in extra_context.items():
1112 response = HttpResponse(t.render(c), mimetype=mimetype)
1114 request, response, model, getattr(obj, obj._meta.pk.name))
1118 @require_http_methods(["GET", "POST"])
1119 @valid_astakos_user_required
1120 def group_search(request, extra_context=None, **kwargs):
1121 q = request.GET.get('q')
1122 if request.method == 'GET':
1123 form = AstakosGroupSearchForm({'q': q} if q else None)
1125 form = AstakosGroupSearchForm(get_query(request))
1127 q = form.cleaned_data['q'].strip()
1129 sorting = 'groupname'
1130 order_dict = {'ASC':'', 'DESC':'-' }
1132 sort_order = 'groupname'
1134 queryset = AstakosGroup.objects.select_related()
1135 queryset = queryset.filter(~Q(kind__name='default'))
1136 queryset = queryset.filter(name__contains=q)
1137 queryset = queryset.filter(approval_date__isnull=False)
1138 queryset = queryset.extra(select={
1139 'groupname': "auth_group.name",
1140 'kindname': "im_groupkind.name",
1141 'approved_members_num': """
1142 SELECT COUNT(*) FROM im_membership
1143 WHERE group_id = im_astakosgroup.group_ptr_id
1144 AND date_joined IS NOT NULL""",
1145 'membership_approval_date': """
1146 SELECT date_joined FROM im_membership
1147 WHERE group_id = im_astakosgroup.group_ptr_id
1148 AND person_id = %s""" % request.user.id,
1150 SELECT CASE WHEN EXISTS(
1151 SELECT date_joined FROM im_membership
1152 WHERE group_id = im_astakosgroup.group_ptr_id
1154 THEN 1 ELSE 0 END""" % request.user.id,
1156 SELECT CASE WHEN EXISTS(
1157 SELECT id FROM im_astakosuser_owner
1158 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1159 AND astakosuser_id = %s)
1160 THEN 1 ELSE 0 END""" % request.user.id,
1161 'is_owner': """SELECT CASE WHEN EXISTS(
1162 SELECT id FROM im_astakosuser_owner
1163 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1164 AND astakosuser_id = %s)
1165 THEN 1 ELSE 0 END""" % request.user.id,
1171 sort_form = AstakosGroupSortForm(request.GET)
1172 if sort_form.is_valid():
1173 sorting = sort_form.cleaned_data.get('sorting')
1174 ordering = request.GET.get('ordering','ASC')
1175 sort_order = order_dict[ordering]+sorting
1176 queryset = queryset.order_by(sort_order)
1179 queryset = AstakosGroup.objects.none()
1183 paginate_by=PAGINATE_BY_ALL,
1184 page=request.GET.get('page') or 1,
1185 template_name='im/astakosgroup_list.html',
1186 extra_context=dict(form=form,
1193 @require_http_methods(["GET", "POST"])
1194 @valid_astakos_user_required
1195 def group_all(request, extra_context=None, **kwargs):
1196 q = AstakosGroup.objects.select_related()
1197 q = q.filter(~Q(kind__name='default'))
1198 q = q.filter(approval_date__isnull=False)
1199 q = q.extra(select={
1200 'groupname': "auth_group.name",
1201 'kindname': "im_groupkind.name",
1202 'approved_members_num': """
1203 SELECT COUNT(*) FROM im_membership
1204 WHERE group_id = im_astakosgroup.group_ptr_id
1205 AND date_joined IS NOT NULL""",
1206 'membership_approval_date': """
1207 SELECT date_joined FROM im_membership
1208 WHERE group_id = im_astakosgroup.group_ptr_id
1209 AND person_id = %s""" % request.user.id,
1211 SELECT CASE WHEN EXISTS(
1212 SELECT date_joined FROM im_membership
1213 WHERE group_id = im_astakosgroup.group_ptr_id
1215 THEN 1 ELSE 0 END""" % request.user.id,
1216 'is_owner': """SELECT CASE WHEN EXISTS(
1217 SELECT id FROM im_astakosuser_owner
1218 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1219 AND astakosuser_id = %s)
1220 THEN 1 ELSE 0 END""" % request.user.id, })
1223 sorting = 'groupname'
1224 order_dict = {'ASC':'', 'DESC':'-' }
1226 sort_order = 'groupname'
1227 sort_form = AstakosGroupSortForm(request.GET)
1229 if sort_form.is_valid():
1230 sorting = sort_form.cleaned_data.get('sorting')
1231 ordering = request.GET.get('ordering','ASC')
1232 sort_order = order_dict[ordering]+sorting
1233 q = q.order_by(sort_order)
1238 paginate_by=PAGINATE_BY_ALL,
1239 page=request.GET.get('page') or 1,
1240 template_name='im/astakosgroup_list.html',
1241 extra_context=dict(form=AstakosGroupSearchForm(),
1247 #@require_http_methods(["POST"])
1248 @require_http_methods(["POST", "GET"])
1249 @valid_astakos_user_required
1250 def group_join(request, group_id):
1251 m = Membership(group_id=group_id,
1252 person=request.user,
1253 date_requested=datetime.now())
1256 post_save_redirect = reverse(
1258 kwargs=dict(group_id=group_id))
1259 return HttpResponseRedirect(post_save_redirect)
1260 except IntegrityError, e:
1262 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1263 messages.error(request, msg)
1264 return group_search(request)
1267 @require_http_methods(["POST"])
1268 @valid_astakos_user_required
1269 def group_leave(request, group_id):
1271 m = Membership.objects.select_related().get(
1273 person=request.user)
1274 except Membership.DoesNotExist:
1275 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1276 if request.user in m.group.owner.all():
1277 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1278 return delete_object(
1282 template_name='im/astakosgroup_list.html',
1283 post_delete_redirect=reverse(
1285 kwargs=dict(group_id=group_id)))
1288 def handle_membership(func):
1290 def wrapper(request, group_id, user_id):
1292 m = Membership.objects.select_related().get(
1295 except Membership.DoesNotExist:
1296 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1298 if request.user not in m.group.owner.all():
1299 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1301 return group_detail(request, group_id)
1305 #@require_http_methods(["POST"])
1306 @require_http_methods(["POST", "GET"])
1307 @valid_astakos_user_required
1309 def approve_member(request, membership):
1311 membership.approve()
1312 realname = membership.person.realname
1313 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1314 messages.success(request, msg)
1315 except AssertionError:
1316 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1317 messages.error(request, msg)
1318 except BaseException, e:
1320 realname = membership.person.realname
1321 msg = _(astakos_messages.GENERIC_ERROR)
1322 messages.error(request, msg)
1325 @valid_astakos_user_required
1327 def disapprove_member(request, membership):
1329 membership.disapprove()
1330 realname = membership.person.realname
1331 msg = astakos_messages.MEMBER_REMOVED % locals()
1332 messages.success(request, msg)
1333 except BaseException, e:
1335 msg = _(astakos_messages.GENERIC_ERROR)
1336 messages.error(request, msg)
1339 #@require_http_methods(["GET"])
1340 @require_http_methods(["POST", "GET"])
1341 @valid_astakos_user_required
1342 def resource_usage(request):
1343 def with_class(entry):
1344 entry['load_class'] = 'red'
1345 max_value = float(entry['maxValue'])
1346 curr_value = float(entry['currValue'])
1347 entry['ratio_limited']= 0
1349 entry['ratio'] = (curr_value / max_value) * 100
1352 if entry['ratio'] < 66:
1353 entry['load_class'] = 'yellow'
1354 if entry['ratio'] < 33:
1355 entry['load_class'] = 'green'
1356 if entry['ratio']<0:
1358 if entry['ratio']>100:
1359 entry['ratio_limited'] = 100
1361 entry['ratio_limited'] = entry['ratio']
1365 def pluralize(entry):
1366 entry['plural'] = engine.plural(entry.get('name'))
1369 result = callpoint.get_user_usage(request.user.id)
1370 if result.is_success:
1371 backenddata = map(with_class, result.data)
1372 data = map(pluralize, result.data)
1375 messages.error(request, result.reason)
1376 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1377 resource_catalog.update_from_result_report(result)
1378 return render_response('im/resource_usage.html',
1380 context_instance=get_context(request),
1381 resource_catalog=resource_catalog,
1385 def group_create_list(request):
1386 form = PickResourceForm()
1387 return render_response(
1388 template='im/astakosgroup_create_list.html',
1389 context_instance=get_context(request),)
1392 ##@require_http_methods(["GET"])
1393 #@require_http_methods(["POST", "GET"])
1394 #@signed_terms_required
1396 #def billing(request):
1398 # today = datetime.today()
1399 # month_last_day = calendar.monthrange(today.year, today.month)[1]
1400 # start = request.POST.get('datefrom', None)
1402 # today = datetime.fromtimestamp(int(start))
1403 # month_last_day = calendar.monthrange(today.year, today.month)[1]
1405 # start = datetime(today.year, today.month, 1).strftime("%s")
1406 # end = datetime(today.year, today.month, month_last_day).strftime("%s")
1407 # r = request_billing.apply(args=('pgerakios@grnet.gr',
1408 # int(start) * 1000,
1413 # status, data = r.result
1414 # data = _clear_billing_data(data)
1416 # messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1418 # messages.error(request, r.result)
1420 # return render_response(
1421 # template='im/billing.html',
1422 # context_instance=get_context(request),
1424 # zerodate=datetime(month=1, year=1970, day=1),
1427 # month_last_day=month_last_day)
1430 #def _clear_billing_data(data):
1432 # # remove addcredits entries
1433 # def isnotcredit(e):
1434 # return e['serviceName'] != "addcredits"
1436 # # separate services
1437 # def servicefilter(service_name):
1438 # service = service_name
1441 # return e['serviceName'] == service
1444 # data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1445 # data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1446 # data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1447 # data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1452 #@require_http_methods(["GET"])
1453 @require_http_methods(["POST", "GET"])
1454 @valid_astakos_user_required
1455 def timeline(request):
1456 # data = {'entity':request.user.email}
1458 timeline_header = ()
1459 # form = TimelineForm(data)
1460 form = TimelineForm()
1461 if request.method == 'POST':
1463 form = TimelineForm(data)
1465 data = form.cleaned_data
1466 timeline_header = ('entity', 'resource',
1467 'event name', 'event date',
1468 'incremental cost', 'total cost')
1469 timeline_body = timeline_charge(
1470 data['entity'], data['resource'],
1471 data['start_date'], data['end_date'],
1472 data['details'], data['operation'])
1474 return render_response(template='im/timeline.html',
1475 context_instance=get_context(request),
1477 timeline_header=timeline_header,
1478 timeline_body=timeline_body)
1482 # TODO: action only on POST and user should confirm the removal
1483 @require_http_methods(["GET", "POST"])
1485 @signed_terms_required
1486 def remove_auth_provider(request, pk):
1488 provider = request.user.auth_providers.get(pk=pk)
1489 except AstakosUserAuthProvider.DoesNotExist:
1492 if provider.can_remove():
1494 return HttpResponseRedirect(reverse('edit_profile'))
1496 raise PermissionDenied
1499 def how_it_works(request):
1500 return render_response(
1501 template='im/how_it_works.html',
1502 context_instance=get_context(request),)