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,
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 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
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 raise PermissionDenied
131 return func(request, *args)
136 def requires_anonymous(func):
138 Decorator checkes whether the request.user is not Anonymous and in that case
139 redirects to `logout`.
142 def wrapper(request, *args):
143 if not request.user.is_anonymous():
144 next = urlencode({'next': request.build_absolute_uri()})
145 logout_uri = reverse(logout) + '?' + next
146 return HttpResponseRedirect(logout_uri)
147 return func(request, *args)
151 def signed_terms_required(func):
153 Decorator checkes whether the request.user is Anonymous and in that case
154 redirects to `logout`.
157 def wrapper(request, *args, **kwargs):
158 if request.user.is_authenticated() and not request.user.signed_terms:
159 params = urlencode({'next': request.build_absolute_uri(),
161 terms_uri = reverse('latest_terms') + '?' + params
162 return HttpResponseRedirect(terms_uri)
163 return func(request, *args, **kwargs)
167 @require_http_methods(["GET", "POST"])
168 @signed_terms_required
169 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
171 If there is logged on user renders the profile page otherwise renders login page.
175 ``login_template_name``
176 A custom login template to use. This is optional; if not specified,
177 this will default to ``im/login.html``.
179 ``profile_template_name``
180 A custom profile template to use. This is optional; if not specified,
181 this will default to ``im/profile.html``.
184 An dictionary of variables to add to the template context.
188 im/profile.html or im/login.html or ``template_name`` keyword argument.
191 extra_context = extra_context or {}
192 template_name = login_template_name
193 if request.user.is_authenticated():
194 return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
196 return render_response(
198 login_form = LoginForm(request=request),
199 context_instance = get_context(request, extra_context)
203 @require_http_methods(["GET", "POST"])
205 @signed_terms_required
206 @transaction.commit_manually
207 def invite(request, template_name='im/invitations.html', extra_context=None):
209 Allows a user to invite somebody else.
211 In case of GET request renders a form for providing the invitee information.
212 In case of POST checks whether the user has not run out of invitations and then
213 sends an invitation email to singup to the service.
215 The view uses commit_manually decorator in order to ensure the number of the
216 user invitations is going to be updated only if the email has been successfully sent.
218 If the user isn't logged in, redirects to settings.LOGIN_URL.
223 A custom template to use. This is optional; if not specified,
224 this will default to ``im/invitations.html``.
227 An dictionary of variables to add to the template context.
231 im/invitations.html or ``template_name`` keyword argument.
235 The view expectes the following settings are defined:
237 * LOGIN_URL: login uri
239 extra_context = extra_context or {}
242 form = InvitationForm()
244 inviter = request.user
245 if request.method == 'POST':
246 form = InvitationForm(request.POST)
247 if inviter.invitations > 0:
250 email = form.cleaned_data.get('username')
251 realname = form.cleaned_data.get('realname')
252 inviter.invite(email, realname)
253 message = _(astakos_messages.INVITATION_SENT) % locals()
254 messages.success(request, message)
255 except SendMailError, e:
257 messages.error(request, message)
258 transaction.rollback()
259 except BaseException, e:
260 message = _(astakos_messages.GENERIC_ERROR)
261 messages.error(request, message)
263 transaction.rollback()
267 message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
268 messages.error(request, message)
270 sent = [{'email': inv.username,
271 'realname': inv.realname,
272 'is_consumed': inv.is_consumed}
273 for inv in request.user.invitations_sent.all()]
274 kwargs = {'inviter': inviter,
276 context = get_context(request, extra_context, **kwargs)
277 return render_response(template_name,
278 invitation_form=form,
279 context_instance=context)
282 @require_http_methods(["GET", "POST"])
284 @signed_terms_required
285 def edit_profile(request, template_name='im/profile.html', extra_context=None):
287 Allows a user to edit his/her profile.
289 In case of GET request renders a form for displaying the user information.
290 In case of POST updates the user informantion and redirects to ``next``
291 url parameter if exists.
293 If the user isn't logged in, redirects to settings.LOGIN_URL.
298 A custom template to use. This is optional; if not specified,
299 this will default to ``im/profile.html``.
302 An dictionary of variables to add to the template context.
306 im/profile.html or ``template_name`` keyword argument.
310 The view expectes the following settings are defined:
312 * LOGIN_URL: login uri
314 extra_context = extra_context or {}
316 instance=request.user,
317 session_key=request.session.session_key
319 extra_context['next'] = request.GET.get('next')
320 if request.method == 'POST':
323 instance=request.user,
324 session_key=request.session.session_key
328 prev_token = request.user.auth_token
332 session_key=request.session.session_key
334 next = restrict_next(
335 request.POST.get('next'),
339 return redirect(next)
340 msg = _(astakos_messages.PROFILE_UPDATED)
341 messages.success(request, msg)
342 except ValueError, ve:
343 messages.success(request, ve)
344 elif request.method == "GET":
345 request.user.is_verified = True
349 user_providers = request.user.get_active_auth_providers()
351 # providers that user can add
352 user_available_providers = request.user.get_available_auth_providers()
354 return render_response(template_name,
356 user_providers = user_providers,
357 user_available_providers = user_available_providers,
358 context_instance = get_context(request,
362 @transaction.commit_manually
363 @require_http_methods(["GET", "POST"])
364 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
366 Allows a user to create a local account.
368 In case of GET request renders a form for entering the user information.
369 In case of POST handles the signup.
371 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
372 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
373 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
374 (see activation_backends);
376 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
377 otherwise renders the same page with a success message.
379 On unsuccessful creation, renders ``template_name`` with an error message.
384 A custom template to render. This is optional;
385 if not specified, this will default to ``im/signup.html``.
388 A custom template to render in case of success. This is optional;
389 if not specified, this will default to ``im/signup_complete.html``.
392 An dictionary of variables to add to the template context.
396 im/signup.html or ``template_name`` keyword argument.
397 im/signup_complete.html or ``on_success`` keyword argument.
399 extra_context = extra_context or {}
400 if request.user.is_authenticated():
401 return HttpResponseRedirect(reverse('edit_profile'))
403 provider = get_query(request).get('provider', 'local')
404 if not auth_providers.get_provider(provider).is_available_for_create():
405 raise PermissionDenied
407 id = get_query(request).get('id')
409 instance = AstakosUser.objects.get(id=id) if id else None
410 except AstakosUser.DoesNotExist:
415 backend = get_backend(request)
416 form = backend.get_signup_form(provider, instance)
418 form = SimpleBackend(request).get_signup_form(provider)
419 messages.error(request, e)
420 if request.method == 'POST':
422 user = form.save(commit=False)
424 result = backend.handle_activation(user)
425 status = messages.SUCCESS
426 message = result.message
428 form.store_user(user, request)
430 if 'additional_email' in form.cleaned_data:
431 additional_email = form.cleaned_data['additional_email']
432 if additional_email != user.email:
433 user.additionalmail_set.create(email=additional_email)
434 msg = 'Additional email: %s saved for user %s.' % (
438 logger._log(LOGGING_LEVEL, msg, [])
439 if user and user.is_active:
440 next = request.POST.get('next', '')
441 response = prepare_response(request, user, next=next)
444 messages.add_message(request, status, message)
446 return render_response(
448 context_instance=get_context(
453 except SendMailError, e:
455 status = messages.ERROR
457 messages.error(request, message)
458 transaction.rollback()
459 except BaseException, e:
461 message = _(astakos_messages.GENERIC_ERROR)
462 messages.error(request, message)
464 transaction.rollback()
465 return render_response(template_name,
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 return render_response(
834 template='im/astakosgroup_form_summary.html',
835 context_instance=get_context(request),
836 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
837 policies = resource_catalog.get_policies(form.policies()),
838 resource_catalog= resource_catalog,
846 for group, resources in resource_catalog.get_groups_resources():
847 data['is_selected_%s' % group] = False
848 for resource in resources:
849 data['%s_uplimit' % resource] = ''
851 form = form_class(data)
853 # Create the template, context, response
854 template_name = "%s/%s_form.html" % (
855 model._meta.app_label,
856 model._meta.object_name.lower()
858 t = template_loader.get_template(template_name)
859 c = RequestContext(request, {
862 'resource_catalog':resource_catalog,
863 }, context_processors)
864 return HttpResponse(t.render(c))
867 #@require_http_methods(["POST"])
868 @require_http_methods(["GET", "POST"])
869 @signed_terms_required
871 def group_add_complete(request):
873 form = AstakosGroupCreationSummaryForm(request.POST)
875 d = form.cleaned_data
876 d['owners'] = [request.user]
877 result = callpoint.create_groups((d,)).next()
878 if result.is_success:
879 new_object = result.data[0]
880 msg = _(astakos_messages.OBJECT_CREATED) %\
881 {"verbose_name": model._meta.verbose_name}
882 messages.success(request, msg, fail_silently=True)
886 send_group_creation_notification(
887 template_name='im/group_creation_notification.txt',
890 'owner': request.user,
891 'policies': d.get('policies', [])
894 except SendNotificationError, e:
895 messages.error(request, e, fail_silently=True)
896 post_save_redirect = '/im/group/%(id)s/'
897 return HttpResponseRedirect(post_save_redirect % new_object)
899 d = {"verbose_name": model._meta.verbose_name,
900 "reason":result.reason}
901 msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
902 messages.error(request, msg, fail_silently=True)
903 return render_response(
904 template='im/astakosgroup_form_summary.html',
905 context_instance=get_context(request),
909 #@require_http_methods(["GET"])
910 @require_http_methods(["GET", "POST"])
911 @signed_terms_required
913 def group_list(request):
914 none = request.user.astakos_groups.none()
915 sorting = request.GET.get('sorting')
917 SELECT auth_group.id,
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 = %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 = %s
940 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)
943 query = query+" ORDER BY %s ASC" %sorting
945 query = query+" ORDER BY groupname ASC"
946 q = AstakosGroup.objects.raw(query)
950 # Create the template, context, response
951 template_name = "%s/%s_list.html" % (
952 q.model._meta.app_label,
953 q.model._meta.object_name.lower()
955 extra_context = dict(
958 sorting=request.GET.get('sorting'),
959 page=request.GET.get('page', 1)
961 return render_response(template_name,
962 context_instance=get_context(request, extra_context)
966 @require_http_methods(["GET", "POST"])
967 @signed_terms_required
969 def group_detail(request, group_id):
970 q = AstakosGroup.objects.select_related().filter(pk=group_id)
972 'is_member': """SELECT CASE WHEN EXISTS(
973 SELECT id FROM im_membership
974 WHERE group_id = im_astakosgroup.group_ptr_id
976 THEN 1 ELSE 0 END""" % request.user.id,
977 'is_owner': """SELECT CASE WHEN EXISTS(
978 SELECT id FROM im_astakosuser_owner
979 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
980 AND astakosuser_id = %s)
981 THEN 1 ELSE 0 END""" % request.user.id,
982 'kindname': """SELECT name FROM im_groupkind
983 WHERE id = im_astakosgroup.kind_id"""})
986 context_processors = None
990 except AstakosGroup.DoesNotExist:
991 raise Http404("No %s found matching the query" % (
992 model._meta.verbose_name))
994 update_form = AstakosGroupUpdateForm(instance=obj)
995 addmembers_form = AddGroupMembersForm()
996 if request.method == 'POST':
999 for k, v in request.POST.iteritems():
1000 if k in update_form.fields:
1002 if k in addmembers_form.fields:
1003 addmembers_data[k] = v
1004 update_data = update_data or None
1005 addmembers_data = addmembers_data or None
1006 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1007 addmembers_form = AddGroupMembersForm(addmembers_data)
1008 if update_form.is_valid():
1010 if addmembers_form.is_valid():
1012 map(obj.approve_member, addmembers_form.valid_users)
1013 except AssertionError:
1014 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1015 messages.error(request, msg)
1016 addmembers_form = AddGroupMembersForm()
1018 template_name = "%s/%s_detail.html" % (
1019 model._meta.app_label, model._meta.object_name.lower())
1020 t = template_loader.get_template(template_name)
1021 c = RequestContext(request, {
1023 }, context_processors)
1026 sorting = request.GET.get('sorting')
1028 form = MembersSortForm({'sort_by': sorting})
1030 sorting = form.cleaned_data.get('sort_by')
1033 form = MembersSortForm({'sort_by': 'person_first_name'})
1035 result = callpoint.list_resources()
1036 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1037 resource_catalog.update_from_result(result)
1040 if not result.is_success:
1043 'Unable to retrieve system resources: %s' % result.reason
1046 extra_context = {'update_form': update_form,
1047 'addmembers_form': addmembers_form,
1048 'page': request.GET.get('page', 1),
1050 'resource_catalog':resource_catalog,
1051 'quota':resource_catalog.get_quota(obj.quota)}
1052 for key, value in extra_context.items():
1057 response = HttpResponse(t.render(c), mimetype=mimetype)
1059 request, response, model, getattr(obj, obj._meta.pk.name))
1063 @require_http_methods(["GET", "POST"])
1064 @signed_terms_required
1066 def group_search(request, extra_context=None, **kwargs):
1067 q = request.GET.get('q')
1068 sorting = request.GET.get('sorting')
1069 if request.method == 'GET':
1070 form = AstakosGroupSearchForm({'q': q} if q else None)
1072 form = AstakosGroupSearchForm(get_query(request))
1074 q = form.cleaned_data['q'].strip()
1076 queryset = AstakosGroup.objects.select_related()
1077 queryset = queryset.filter(name__contains=q)
1078 queryset = queryset.filter(approval_date__isnull=False)
1079 queryset = queryset.extra(select={
1080 'groupname': DB_REPLACE_GROUP_SCHEME,
1081 'kindname': "im_groupkind.name",
1082 'approved_members_num': """
1083 SELECT COUNT(*) FROM im_membership
1084 WHERE group_id = im_astakosgroup.group_ptr_id
1085 AND date_joined IS NOT NULL""",
1086 'membership_approval_date': """
1087 SELECT date_joined FROM im_membership
1088 WHERE group_id = im_astakosgroup.group_ptr_id
1089 AND person_id = %s""" % request.user.id,
1091 SELECT CASE WHEN EXISTS(
1092 SELECT date_joined FROM im_membership
1093 WHERE group_id = im_astakosgroup.group_ptr_id
1095 THEN 1 ELSE 0 END""" % request.user.id,
1097 SELECT CASE WHEN EXISTS(
1098 SELECT id FROM im_astakosuser_owner
1099 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1100 AND astakosuser_id = %s)
1101 THEN 1 ELSE 0 END""" % request.user.id,
1102 'is_owner': """SELECT CASE WHEN EXISTS(
1103 SELECT id FROM im_astakosuser_owner
1104 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1105 AND astakosuser_id = %s)
1106 THEN 1 ELSE 0 END""" % request.user.id,
1109 # TODO check sorting value
1110 queryset = queryset.order_by(sorting)
1112 queryset = queryset.order_by("groupname")
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': DB_REPLACE_GROUP_SCHEME,
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, })
1156 sorting = request.GET.get('sorting')
1158 # TODO check sorting value
1159 q = q.order_by(sorting)
1161 q = q.order_by("groupname")
1166 paginate_by=PAGINATE_BY_ALL,
1167 page=request.GET.get('page') or 1,
1168 template_name='im/astakosgroup_list.html',
1169 extra_context=dict(form=AstakosGroupSearchForm(),
1174 #@require_http_methods(["POST"])
1175 @require_http_methods(["POST", "GET"])
1176 @signed_terms_required
1178 def group_join(request, group_id):
1179 m = Membership(group_id=group_id,
1180 person=request.user,
1181 date_requested=datetime.now())
1184 post_save_redirect = reverse(
1186 kwargs=dict(group_id=group_id))
1187 return HttpResponseRedirect(post_save_redirect)
1188 except IntegrityError, e:
1190 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1191 messages.error(request, msg)
1192 return group_search(request)
1195 @require_http_methods(["POST"])
1196 @signed_terms_required
1198 def group_leave(request, group_id):
1200 m = Membership.objects.select_related().get(
1202 person=request.user)
1203 except Membership.DoesNotExist:
1204 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1205 if request.user in m.group.owner.all():
1206 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1207 return delete_object(
1211 template_name='im/astakosgroup_list.html',
1212 post_delete_redirect=reverse(
1214 kwargs=dict(group_id=group_id)))
1217 def handle_membership(func):
1219 def wrapper(request, group_id, user_id):
1221 m = Membership.objects.select_related().get(
1224 except Membership.DoesNotExist:
1225 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1227 if request.user not in m.group.owner.all():
1228 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1230 return group_detail(request, group_id)
1234 #@require_http_methods(["POST"])
1235 @require_http_methods(["POST", "GET"])
1236 @signed_terms_required
1239 def approve_member(request, membership):
1241 membership.approve()
1242 realname = membership.person.realname
1243 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1244 messages.success(request, msg)
1245 except AssertionError:
1246 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1247 messages.error(request, msg)
1248 except BaseException, e:
1250 realname = membership.person.realname
1251 msg = _(astakos_messages.GENERIC_ERROR)
1252 messages.error(request, msg)
1255 @signed_terms_required
1258 def disapprove_member(request, membership):
1260 membership.disapprove()
1261 realname = membership.person.realname
1262 msg = astakos_messages.MEMBER_REMOVED % realname
1263 messages.success(request, msg)
1264 except BaseException, e:
1266 msg = _(astakos_messages.GENERIC_ERROR)
1267 messages.error(request, msg)
1270 #@require_http_methods(["GET"])
1271 @require_http_methods(["POST", "GET"])
1272 @signed_terms_required
1274 def resource_list(request):
1275 def with_class(entry):
1276 entry['load_class'] = 'red'
1277 max_value = float(entry['maxValue'])
1278 curr_value = float(entry['currValue'])
1280 entry['ratio'] = (curr_value / max_value) * 100
1283 if entry['ratio'] < 66:
1284 entry['load_class'] = 'yellow'
1285 if entry['ratio'] < 33:
1286 entry['load_class'] = 'green'
1289 def pluralize(entry):
1290 entry['plural'] = engine.plural(entry.get('name'))
1293 result = callpoint.get_user_status(request.user.id)
1294 if result.is_success:
1295 backenddata = map(with_class, result.data)
1296 data = map(pluralize, result.data)
1299 messages.error(request, result.reason)
1300 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1301 resource_catalog.update_from_result_report(result)
1305 return render_response('im/resource_list.html',
1307 context_instance=get_context(request),
1308 resource_catalog=resource_catalog,
1312 def group_create_list(request):
1313 form = PickResourceForm()
1314 return render_response(
1315 template='im/astakosgroup_create_list.html',
1316 context_instance=get_context(request),)
1319 #@require_http_methods(["GET"])
1320 @require_http_methods(["POST", "GET"])
1321 @signed_terms_required
1323 def billing(request):
1325 today = datetime.today()
1326 month_last_day = calendar.monthrange(today.year, today.month)[1]
1327 start = request.POST.get('datefrom', None)
1329 today = datetime.fromtimestamp(int(start))
1330 month_last_day = calendar.monthrange(today.year, today.month)[1]
1332 start = datetime(today.year, today.month, 1).strftime("%s")
1333 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1334 r = request_billing.apply(args=('pgerakios@grnet.gr',
1340 status, data = r.result
1341 data = _clear_billing_data(data)
1343 messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1345 messages.error(request, r.result)
1347 return render_response(
1348 template='im/billing.html',
1349 context_instance=get_context(request),
1351 zerodate=datetime(month=1, year=1970, day=1),
1354 month_last_day=month_last_day)
1357 def _clear_billing_data(data):
1359 # remove addcredits entries
1361 return e['serviceName'] != "addcredits"
1364 def servicefilter(service_name):
1365 service = service_name
1368 return e['serviceName'] == service
1371 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1372 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1373 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1374 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1379 #@require_http_methods(["GET"])
1380 @require_http_methods(["POST", "GET"])
1381 @signed_terms_required
1383 def timeline(request):
1384 # data = {'entity':request.user.email}
1386 timeline_header = ()
1387 # form = TimelineForm(data)
1388 form = TimelineForm()
1389 if request.method == 'POST':
1391 form = TimelineForm(data)
1393 data = form.cleaned_data
1394 timeline_header = ('entity', 'resource',
1395 'event name', 'event date',
1396 'incremental cost', 'total cost')
1397 timeline_body = timeline_charge(
1398 data['entity'], data['resource'],
1399 data['start_date'], data['end_date'],
1400 data['details'], data['operation'])
1402 return render_response(template='im/timeline.html',
1403 context_instance=get_context(request),
1405 timeline_header=timeline_header,
1406 timeline_body=timeline_body)
1409 # TODO: action only on POST and user should confirm the removal
1410 @require_http_methods(["GET", "POST"])
1412 @signed_terms_required
1413 def remove_auth_provider(request, pk):
1415 provider = request.user.auth_providers.get(pk=pk)
1416 except AstakosUserAuthProvider.DoesNotExist:
1419 if provider.can_remove():
1421 return HttpResponseRedirect(reverse('edit_profile'))
1423 raise PermissionDenied