1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
38 engine = inflect.engine()
40 from urllib import quote
41 from functools import wraps
42 from datetime import datetime, timedelta
43 from collections import defaultdict
45 from django.contrib import messages
46 from django.contrib.auth.decorators import login_required
47 from django.contrib.auth.views import password_change
48 from django.core.urlresolvers import reverse
49 from django.db import transaction
50 from django.db.models import Q
51 from django.db.utils import IntegrityError
52 from django.forms.fields import URLField
53 from django.http import (HttpResponse, HttpResponseBadRequest,
54 HttpResponseForbidden, HttpResponseRedirect,
55 HttpResponseBadRequest, Http404)
56 from django.shortcuts import redirect
57 from django.template import RequestContext, loader as template_loader
58 from django.utils.http import urlencode
59 from django.utils.translation import ugettext as _
60 from django.views.generic.create_update import (create_object, delete_object,
61 get_model_and_form_class)
62 from django.views.generic.list_detail import object_list, object_detail
63 from django.http import HttpResponseBadRequest
64 from django.core.xheaders import populate_xheaders
66 from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
67 Resource, EmailChange, GroupKind, Membership,
68 AstakosGroupQuota, RESOURCE_SEPARATOR)
69 from django.views.decorators.http import require_http_methods
70 from django.db.models.query import QuerySet
72 from astakos.im.activation_backends import get_backend, SimpleBackend
73 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
74 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
75 FeedbackForm, SignApprovalTermsForm,
76 ExtendedPasswordChangeForm, EmailChangeForm,
77 AstakosGroupCreationForm, AstakosGroupSearchForm,
78 AstakosGroupUpdateForm, AddGroupMembersForm,
79 AstakosGroupSortForm, MembersSortForm,
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 switch_account_to_shibboleth,
86 send_group_creation_notification,
87 SendNotificationError)
88 from astakos.im.endpoints.quotaholder import timeline_charge
89 from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
90 LOGGING_LEVEL, PAGINATE_BY)
91 from astakos.im.tasks import request_billing
92 from astakos.im.api.callpoint import AstakosCallpoint
94 import astakos.im.messages as astakos_messages
96 logger = logging.getLogger(__name__)
99 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
102 callpoint = AstakosCallpoint()
104 def render_response(template, tab=None, status=200, reset_cookie=False,
105 context_instance=None, **kwargs):
107 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
108 keyword argument and returns an ``django.http.HttpResponse`` with the
109 specified ``status``.
112 tab = template.partition('_')[0].partition('.html')[0]
113 kwargs.setdefault('tab', tab)
114 html = template_loader.render_to_string(
115 template, kwargs, context_instance=context_instance)
116 response = HttpResponse(html, status=status)
118 set_cookie(response, context_instance['request'].user)
122 def requires_anonymous(func):
124 Decorator checkes whether the request.user is not Anonymous and in that case
125 redirects to `logout`.
128 def wrapper(request, *args):
129 if not request.user.is_anonymous():
130 next = urlencode({'next': request.build_absolute_uri()})
131 logout_uri = reverse(logout) + '?' + next
132 return HttpResponseRedirect(logout_uri)
133 return func(request, *args)
137 def signed_terms_required(func):
139 Decorator checkes whether the request.user is Anonymous and in that case
140 redirects to `logout`.
143 def wrapper(request, *args, **kwargs):
144 if request.user.is_authenticated() and not request.user.signed_terms:
145 params = urlencode({'next': request.build_absolute_uri(),
147 terms_uri = reverse('latest_terms') + '?' + params
148 return HttpResponseRedirect(terms_uri)
149 return func(request, *args, **kwargs)
153 @require_http_methods(["GET", "POST"])
154 @signed_terms_required
155 def index(request, login_template_name='im/login.html', extra_context=None):
157 If there is logged on user renders the profile page otherwise renders login page.
161 ``login_template_name``
162 A custom login template to use. This is optional; if not specified,
163 this will default to ``im/login.html``.
165 ``profile_template_name``
166 A custom profile template to use. This is optional; if not specified,
167 this will default to ``im/profile.html``.
170 An dictionary of variables to add to the template context.
174 im/profile.html or im/login.html or ``template_name`` keyword argument.
177 template_name = login_template_name
178 if request.user.is_authenticated():
179 return HttpResponseRedirect(reverse('edit_profile'))
180 return render_response(template_name,
181 login_form=LoginForm(request=request),
182 context_instance=get_context(request, extra_context))
185 @require_http_methods(["GET", "POST"])
187 @signed_terms_required
188 @transaction.commit_manually
189 def invite(request, template_name='im/invitations.html', extra_context=None):
191 Allows a user to invite somebody else.
193 In case of GET request renders a form for providing the invitee information.
194 In case of POST checks whether the user has not run out of invitations and then
195 sends an invitation email to singup to the service.
197 The view uses commit_manually decorator in order to ensure the number of the
198 user invitations is going to be updated only if the email has been successfully sent.
200 If the user isn't logged in, redirects to settings.LOGIN_URL.
205 A custom template to use. This is optional; if not specified,
206 this will default to ``im/invitations.html``.
209 An dictionary of variables to add to the template context.
213 im/invitations.html or ``template_name`` keyword argument.
217 The view expectes the following settings are defined:
219 * LOGIN_URL: login uri
220 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
224 form = InvitationForm()
226 inviter = request.user
227 if request.method == 'POST':
228 form = InvitationForm(request.POST)
229 if inviter.invitations > 0:
232 email = form.cleaned_data.get('username')
233 realname = form.cleaned_data.get('realname')
234 inviter.invite(email, realname)
235 message = _(astakos_messages.INVITATION_SENT) % locals()
236 messages.success(request, message)
237 except SendMailError, e:
239 messages.error(request, message)
240 transaction.rollback()
241 except BaseException, e:
242 message = _(astakos_messages.GENERIC_ERROR)
243 messages.error(request, message)
245 transaction.rollback()
249 message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
250 messages.error(request, message)
252 sent = [{'email': inv.username,
253 'realname': inv.realname,
254 'is_consumed': inv.is_consumed}
255 for inv in request.user.invitations_sent.all()]
256 kwargs = {'inviter': inviter,
258 context = get_context(request, extra_context, **kwargs)
259 return render_response(template_name,
260 invitation_form=form,
261 context_instance=context)
264 @require_http_methods(["GET", "POST"])
266 @signed_terms_required
267 def edit_profile(request, template_name='im/profile.html', extra_context=None):
269 Allows a user to edit his/her profile.
271 In case of GET request renders a form for displaying the user information.
272 In case of POST updates the user informantion and redirects to ``next``
273 url parameter if exists.
275 If the user isn't logged in, redirects to settings.LOGIN_URL.
280 A custom template to use. This is optional; if not specified,
281 this will default to ``im/profile.html``.
284 An dictionary of variables to add to the template context.
288 im/profile.html or ``template_name`` keyword argument.
292 The view expectes the following settings are defined:
294 * LOGIN_URL: login uri
296 extra_context = extra_context or {}
297 form = ProfileForm(instance=request.user)
298 extra_context['next'] = request.GET.get('next')
300 if request.method == 'POST':
301 form = ProfileForm(request.POST, instance=request.user)
304 prev_token = request.user.auth_token
306 reset_cookie = user.auth_token != prev_token
307 form = ProfileForm(instance=user)
308 next = request.POST.get('next')
310 return redirect(next)
311 msg = _(astakos_messages.PROFILE_UPDATED)
312 messages.success(request, msg)
313 except ValueError, ve:
314 messages.success(request, ve)
315 elif request.method == "GET":
316 if not request.user.is_verified:
317 request.user.is_verified = True
319 return render_response(template_name,
320 reset_cookie=reset_cookie,
322 context_instance=get_context(request,
326 @transaction.commit_manually
327 @require_http_methods(["GET", "POST"])
328 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
330 Allows a user to create a local account.
332 In case of GET request renders a form for entering the user information.
333 In case of POST handles the signup.
335 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
336 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
337 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
338 (see activation_backends);
340 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
341 otherwise renders the same page with a success message.
343 On unsuccessful creation, renders ``template_name`` with an error message.
348 A custom template to render. This is optional;
349 if not specified, this will default to ``im/signup.html``.
352 A custom template to render in case of success. This is optional;
353 if not specified, this will default to ``im/signup_complete.html``.
356 An dictionary of variables to add to the template context.
360 im/signup.html or ``template_name`` keyword argument.
361 im/signup_complete.html or ``on_success`` keyword argument.
363 if request.user.is_authenticated():
364 return HttpResponseRedirect(reverse('edit_profile'))
366 provider = get_query(request).get('provider', 'local')
369 backend = get_backend(request)
370 form = backend.get_signup_form(provider)
372 form = SimpleBackend(request).get_signup_form(provider)
373 messages.error(request, e)
374 if request.method == 'POST':
376 user = form.save(commit=False)
378 result = backend.handle_activation(user)
379 status = messages.SUCCESS
380 message = result.message
382 if 'additional_email' in form.cleaned_data:
383 additional_email = form.cleaned_data['additional_email']
384 if additional_email != user.email:
385 user.additionalmail_set.create(email=additional_email)
386 msg = 'Additional email: %s saved for user %s.' % (
387 additional_email, user.email)
388 logger.log(LOGGING_LEVEL, msg)
389 if user and user.is_active:
390 next = request.POST.get('next', '')
391 response = prepare_response(request, user, next=next)
394 messages.add_message(request, status, message)
396 return render_response(on_success,
397 context_instance=get_context(request, extra_context))
398 except SendMailError, e:
400 messages.error(request, message)
401 transaction.rollback()
402 except BaseException, e:
403 message = _(astakos_messages.GENERIC_ERROR)
404 messages.error(request, message)
406 transaction.rollback()
407 return render_response(template_name,
410 context_instance=get_context(request, extra_context))
413 @require_http_methods(["GET", "POST"])
415 @signed_terms_required
416 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
418 Allows a user to send feedback.
420 In case of GET request renders a form for providing the feedback information.
421 In case of POST sends an email to support team.
423 If the user isn't logged in, redirects to settings.LOGIN_URL.
428 A custom template to use. This is optional; if not specified,
429 this will default to ``im/feedback.html``.
432 An dictionary of variables to add to the template context.
436 im/signup.html or ``template_name`` keyword argument.
440 * LOGIN_URL: login uri
441 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
443 if request.method == 'GET':
444 form = FeedbackForm()
445 if request.method == 'POST':
447 return HttpResponse('Unauthorized', status=401)
449 form = FeedbackForm(request.POST)
451 msg = form.cleaned_data['feedback_msg']
452 data = form.cleaned_data['feedback_data']
454 send_feedback(msg, data, request.user, email_template_name)
455 except SendMailError, e:
456 messages.error(request, message)
458 message = _(astakos_messages.FEEDBACK_SENT)
459 messages.success(request, message)
460 return render_response(template_name,
462 context_instance=get_context(request, extra_context))
465 @require_http_methods(["GET", "POST"])
466 @signed_terms_required
467 def logout(request, template='registration/logged_out.html', extra_context=None):
469 Wraps `django.contrib.auth.logout` and delete the cookie.
471 response = HttpResponse()
472 if request.user.is_authenticated():
473 email = request.user.email
475 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
476 msg = 'Cookie deleted for %s' % email
477 logger.log(LOGGING_LEVEL, msg)
478 next = request.GET.get('next')
480 response['Location'] = next
481 response.status_code = 302
484 response['Location'] = LOGOUT_NEXT
485 response.status_code = 301
487 messages.success(request, _(astakos_messages.LOGOUT_SUCCESS))
488 context = get_context(request, extra_context)
490 template_loader.render_to_string(template, context_instance=context))
494 @require_http_methods(["GET", "POST"])
495 @transaction.commit_manually
496 def activate(request, greeting_email_template_name='im/welcome_email.txt',
497 helpdesk_email_template_name='im/helpdesk_notification.txt'):
499 Activates the user identified by the ``auth`` request parameter, sends a welcome email
500 and renews the user token.
502 The view uses commit_manually decorator in order to ensure the user state will be updated
503 only if the email will be send successfully.
505 token = request.GET.get('auth')
506 next = request.GET.get('next')
508 user = AstakosUser.objects.get(auth_token=token)
509 except AstakosUser.DoesNotExist:
510 return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
513 message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
514 messages.error(request, message)
515 return index(request)
518 local_user = AstakosUser.objects.get(
523 except AstakosUser.DoesNotExist:
527 greeting_email_template_name,
528 helpdesk_email_template_name,
531 response = prepare_response(request, user, next, renew=True)
534 except SendMailError, e:
536 messages.error(request, message)
537 transaction.rollback()
538 return index(request)
539 except BaseException, e:
540 message = _(astakos_messages.GENERIC_ERROR)
541 messages.error(request, message)
543 transaction.rollback()
544 return index(request)
547 user = switch_account_to_shibboleth(
550 greeting_email_template_name
552 response = prepare_response(request, user, next, renew=True)
555 except SendMailError, e:
557 messages.error(request, message)
558 transaction.rollback()
559 return index(request)
560 except BaseException, e:
561 message = _(astakos_messages.GENERIC_ERROR)
562 messages.error(request, message)
564 transaction.rollback()
565 return index(request)
568 @require_http_methods(["GET", "POST"])
569 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
574 term = ApprovalTerms.objects.order_by('-id')[0]
579 term = ApprovalTerms.objects.get(id=term_id)
580 except ApprovalTerms.DoesNotExist, e:
584 messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
585 return HttpResponseRedirect(reverse('index'))
586 f = open(term.location, 'r')
589 if request.method == 'POST':
590 next = request.POST.get('next')
592 next = reverse('index')
593 form = SignApprovalTermsForm(request.POST, instance=request.user)
594 if not form.is_valid():
595 return render_response(template_name,
597 approval_terms_form=form,
598 context_instance=get_context(request, extra_context))
600 return HttpResponseRedirect(next)
603 if request.user.is_authenticated() and not request.user.signed_terms:
604 form = SignApprovalTermsForm(instance=request.user)
605 return render_response(template_name,
607 approval_terms_form=form,
608 context_instance=get_context(request, extra_context))
611 @require_http_methods(["GET", "POST"])
612 @signed_terms_required
613 def change_password(request):
614 return password_change(request,
615 post_change_redirect=reverse('edit_profile'),
616 password_change_form=ExtendedPasswordChangeForm)
619 @require_http_methods(["GET", "POST"])
620 @signed_terms_required
622 @transaction.commit_manually
623 def change_email(request, activation_key=None,
624 email_template_name='registration/email_change_email.txt',
625 form_template_name='registration/email_change_form.html',
626 confirm_template_name='registration/email_change_done.html',
630 user = EmailChange.objects.change_email(activation_key)
631 if request.user.is_authenticated() and request.user == user:
632 msg = _(astakos_messages.EMAIL_CHANGED)
633 messages.success(request, msg)
635 response = prepare_response(request, user)
638 except ValueError, e:
639 messages.error(request, e)
640 return render_response(confirm_template_name,
641 modified_user=user if 'user' in locals(
643 context_instance=get_context(request,
646 if not request.user.is_authenticated():
647 path = quote(request.get_full_path())
648 url = request.build_absolute_uri(reverse('index'))
649 return HttpResponseRedirect(url + '?next=' + path)
650 form = EmailChangeForm(request.POST or None)
651 if request.method == 'POST' and form.is_valid():
653 ec = form.save(email_template_name, request)
654 except SendMailError, e:
656 messages.error(request, msg)
657 transaction.rollback()
658 except IntegrityError, e:
659 msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
660 messages.error(request, msg)
662 msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
663 messages.success(request, msg)
665 return render_response(form_template_name,
667 context_instance=get_context(request,
672 resource_presentation = {
674 'help_text':'group compute help text',
675 'is_abbreviation':False,
679 'help_text':'group storage help text',
680 'is_abbreviation':False,
683 'pithos+.diskspace': {
684 'help_text':'resource pithos+.diskspace help text',
685 'is_abbreviation':False,
686 'report_desc':'Pithos+ Diskspace',
687 'placeholder':'eg. 10GB'
690 'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
691 'is_abbreviation':True,
692 'report_desc':'Virtual Machines',
693 'placeholder':'eg. 2'
695 'cyclades.disksize': {
696 'help_text':'resource cyclades.disksize help text',
697 'is_abbreviation':False,
698 'report_desc':'Disksize',
699 'placeholder':'eg. 5GB, 2GB etc'
702 'help_text':'resource cyclades.disk help text',
703 'is_abbreviation':False,
704 'report_desc':'Disk',
705 'placeholder':'eg. 5GB, 2GB etc'
708 'help_text':'resource cyclades.ram help text',
709 'is_abbreviation':True,
711 'placeholder':'eg. 4GB'
714 'help_text':'resource cyclades.cpu help text',
715 'is_abbreviation':True,
716 'report_desc':'CPUs',
717 'placeholder':'eg. 1'
719 'cyclades.network.private': {
720 'help_text':'resource cyclades.network.private help text',
721 'is_abbreviation':False,
722 'report_desc':'Network',
723 'placeholder':'eg. 1'
727 @require_http_methods(["GET", "POST"])
728 @signed_terms_required
730 def group_add(request, kind_name='default'):
731 result = callpoint.list_resources()
732 resource_catalog = {'resources':defaultdict(defaultdict),
733 'groups':defaultdict(list)}
734 if result.is_success:
735 for r in result.data:
736 service = r.get('service', '')
737 name = r.get('name', '')
738 group = r.get('group', '')
739 unit = r.get('unit', '')
740 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
741 resource_catalog['resources'][fullname] = dict(unit=unit)
742 resource_catalog['groups'][group].append(fullname)
744 resource_catalog = dict(resource_catalog)
745 for k, v in resource_catalog.iteritems():
746 resource_catalog[k] = dict(v)
750 'Unable to retrieve system resources: %s' % result.reason
754 kind = GroupKind.objects.get(name=kind_name)
756 return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
760 post_save_redirect = '/im/group/%(id)s/'
761 context_processors = None
762 model, form_class = get_model_and_form_class(
764 form_class=AstakosGroupCreationForm
767 if request.method == 'POST':
768 form = form_class(request.POST, request.FILES)
770 return render_response(
771 template='im/astakosgroup_form_summary.html',
772 context_instance=get_context(request),
773 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
774 policies = form.policies(),
775 resource_catalog=resource_catalog,
776 resource_presentation=resource_presentation
783 for group, resources in resource_catalog['groups'].iteritems():
784 data['is_selected_%s' % group] = False
785 for resource in resources:
786 data['%s_uplimit' % resource] = ''
788 form = form_class(data)
790 # Create the template, context, response
791 template_name = "%s/%s_form.html" % (
792 model._meta.app_label,
793 model._meta.object_name.lower()
795 t = template_loader.get_template(template_name)
796 c = RequestContext(request, {
799 'resource_catalog':resource_catalog,
800 'resource_presentation':resource_presentation,
801 }, context_processors)
802 return HttpResponse(t.render(c))
805 #@require_http_methods(["POST"])
806 @require_http_methods(["GET", "POST"])
807 @signed_terms_required
809 def group_add_complete(request):
811 form = AstakosGroupCreationSummaryForm(request.POST)
813 d = form.cleaned_data
814 d['owners'] = [request.user]
815 result = callpoint.create_groups((d,)).next()
816 if result.is_success:
817 new_object = result.data[0]
818 msg = _(astakos_messages.OBJECT_CREATED) %\
819 {"verbose_name": model._meta.verbose_name}
820 messages.success(request, msg, fail_silently=True)
824 send_group_creation_notification(
825 template_name='im/group_creation_notification.txt',
828 'owner': request.user,
829 'policies': d.get('policies', [])
832 except SendNotificationError, e:
833 messages.error(request, e, fail_silently=True)
834 post_save_redirect = '/im/group/%(id)s/'
835 return HttpResponseRedirect(post_save_redirect % new_object)
837 msg = _(astakos_messages.OBJECT_CREATED_FAILED) %\
838 {"verbose_name": model._meta.verbose_name,
839 "reason":result.reason}
840 messages.error(request, msg, fail_silently=True)
841 return render_response(
842 template='im/astakosgroup_form_summary.html',
843 context_instance=get_context(request),
847 #@require_http_methods(["GET"])
848 @require_http_methods(["GET", "POST"])
849 @signed_terms_required
851 def group_list(request):
852 none = request.user.astakos_groups.none()
853 q = AstakosGroup.objects.raw("""
854 SELECT auth_group.id,
856 im_groupkind.name AS kindname,
858 owner.email AS groupowner,
859 (SELECT COUNT(*) FROM im_membership
860 WHERE group_id = im_astakosgroup.group_ptr_id
861 AND date_joined IS NOT NULL) AS approved_members_num,
863 SELECT date_joined FROM im_membership
864 WHERE group_id = im_astakosgroup.group_ptr_id
865 AND person_id = %s) IS NULL
866 THEN 0 ELSE 1 END) AS membership_status
868 INNER JOIN im_membership ON (
869 im_astakosgroup.group_ptr_id = im_membership.group_id)
870 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
871 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
872 LEFT JOIN im_astakosuser_owner ON (
873 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
874 LEFT JOIN auth_user as owner ON (
875 im_astakosuser_owner.astakosuser_id = owner.id)
876 WHERE im_membership.person_id = %s
877 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
879 # Create the template, context, response
880 template_name = "%s/%s_list.html" % (
881 q.model._meta.app_label,
882 q.model._meta.object_name.lower()
884 extra_context = dict(
887 sorting=request.GET.get('sorting'),
888 page=request.GET.get('page', 1)
890 return render_response(template_name,
891 context_instance=get_context(request, extra_context)
895 @require_http_methods(["GET", "POST"])
896 @signed_terms_required
898 def group_detail(request, group_id):
899 q = AstakosGroup.objects.select_related().filter(pk=group_id)
901 'is_member': """SELECT CASE WHEN EXISTS(
902 SELECT id FROM im_membership
903 WHERE group_id = im_astakosgroup.group_ptr_id
905 THEN 1 ELSE 0 END""" % request.user.id,
906 'is_owner': """SELECT CASE WHEN EXISTS(
907 SELECT id FROM im_astakosuser_owner
908 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
909 AND astakosuser_id = %s)
910 THEN 1 ELSE 0 END""" % request.user.id,
911 'kindname': """SELECT name FROM im_groupkind
912 WHERE id = im_astakosgroup.kind_id"""})
915 context_processors = None
919 except AstakosGroup.DoesNotExist:
920 raise Http404("No %s found matching the query" % (
921 model._meta.verbose_name))
923 update_form = AstakosGroupUpdateForm(instance=obj)
924 addmembers_form = AddGroupMembersForm()
925 if request.method == 'POST':
928 for k, v in request.POST.iteritems():
929 if k in update_form.fields:
931 if k in addmembers_form.fields:
932 addmembers_data[k] = v
933 update_data = update_data or None
934 addmembers_data = addmembers_data or None
935 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
936 addmembers_form = AddGroupMembersForm(addmembers_data)
937 if update_form.is_valid():
939 if addmembers_form.is_valid():
941 map(obj.approve_member, addmembers_form.valid_users)
942 except AssertionError:
943 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
944 messages.error(request, msg)
945 addmembers_form = AddGroupMembersForm()
947 template_name = "%s/%s_detail.html" % (
948 model._meta.app_label, model._meta.object_name.lower())
949 t = template_loader.get_template(template_name)
950 c = RequestContext(request, {
952 }, context_processors)
955 sorting = request.GET.get('sorting')
957 form = MembersSortForm({'sort_by': sorting})
959 sorting = form.cleaned_data.get('sort_by')
961 result = callpoint.list_resources()
962 resource_catalog = {'resources':defaultdict(defaultdict),
963 'groups':defaultdict(list)}
964 if result.is_success:
965 for r in result.data:
966 service = r.get('service', '')
967 name = r.get('name', '')
968 group = r.get('group', '')
969 unit = r.get('unit', '')
970 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
971 resource_catalog['resources'][fullname] = dict(unit=unit)
972 resource_catalog['groups'][group].append(fullname)
974 resource_catalog = dict(resource_catalog)
975 for k, v in resource_catalog.iteritems():
976 resource_catalog[k] = dict(v)
978 print '####', resource_catalog, obj.quota
979 extra_context = {'update_form': update_form,
980 'addmembers_form': addmembers_form,
981 'page': request.GET.get('page', 1),
983 'resource_catalog':resource_catalog,
984 'resource_presentation':resource_presentation,}
985 for key, value in extra_context.items():
990 response = HttpResponse(t.render(c), mimetype=mimetype)
992 request, response, model, getattr(obj, obj._meta.pk.name))
996 @require_http_methods(["GET", "POST"])
997 @signed_terms_required
999 def group_search(request, extra_context=None, **kwargs):
1000 print '###', request
1001 q = request.GET.get('q')
1002 sorting = request.GET.get('sorting')
1003 if request.method == 'GET':
1004 form = AstakosGroupSearchForm({'q': q} if q else None)
1006 form = AstakosGroupSearchForm(get_query(request))
1008 q = form.cleaned_data['q'].strip()
1010 queryset = AstakosGroup.objects.select_related()
1011 queryset = queryset.filter(name__contains=q)
1012 queryset = queryset.filter(approval_date__isnull=False)
1013 queryset = queryset.extra(select={
1014 'groupname': DB_REPLACE_GROUP_SCHEME,
1015 'kindname': "im_groupkind.name",
1016 'approved_members_num': """
1017 SELECT COUNT(*) FROM im_membership
1018 WHERE group_id = im_astakosgroup.group_ptr_id
1019 AND date_joined IS NOT NULL""",
1020 'membership_approval_date': """
1021 SELECT date_joined FROM im_membership
1022 WHERE group_id = im_astakosgroup.group_ptr_id
1023 AND person_id = %s""" % request.user.id,
1025 SELECT CASE WHEN EXISTS(
1026 SELECT date_joined FROM im_membership
1027 WHERE group_id = im_astakosgroup.group_ptr_id
1029 THEN 1 ELSE 0 END""" % request.user.id,
1031 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_owner': """SELECT CASE WHEN EXISTS(
1037 SELECT id FROM im_astakosuser_owner
1038 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1039 AND astakosuser_id = %s)
1040 THEN 1 ELSE 0 END""" % request.user.id,
1043 # TODO check sorting value
1044 queryset = queryset.order_by(sorting)
1046 queryset = AstakosGroup.objects.none()
1050 paginate_by=PAGINATE_BY,
1051 page=request.GET.get('page') or 1,
1052 template_name='im/astakosgroup_list.html',
1053 extra_context=dict(form=form,
1059 @require_http_methods(["GET", "POST"])
1060 @signed_terms_required
1062 def group_all(request, extra_context=None, **kwargs):
1063 q = AstakosGroup.objects.select_related()
1064 q = q.filter(approval_date__isnull=False)
1065 q = q.extra(select={
1066 'groupname': DB_REPLACE_GROUP_SCHEME,
1067 'kindname': "im_groupkind.name",
1068 'approved_members_num': """
1069 SELECT COUNT(*) FROM im_membership
1070 WHERE group_id = im_astakosgroup.group_ptr_id
1071 AND date_joined IS NOT NULL""",
1072 'membership_approval_date': """
1073 SELECT date_joined FROM im_membership
1074 WHERE group_id = im_astakosgroup.group_ptr_id
1075 AND person_id = %s""" % request.user.id,
1077 SELECT CASE WHEN EXISTS(
1078 SELECT date_joined FROM im_membership
1079 WHERE group_id = im_astakosgroup.group_ptr_id
1081 THEN 1 ELSE 0 END""" % request.user.id,
1082 'is_owner': """SELECT CASE WHEN EXISTS(
1083 SELECT id FROM im_astakosuser_owner
1084 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1085 AND astakosuser_id = %s)
1086 THEN 1 ELSE 0 END""" % request.user.id, })
1087 sorting = request.GET.get('sorting')
1089 # TODO check sorting value
1090 q = q.order_by(sorting)
1094 paginate_by=PAGINATE_BY,
1095 page=request.GET.get('page') or 1,
1096 template_name='im/astakosgroup_list.html',
1097 extra_context=dict(form=AstakosGroupSearchForm(),
1102 #@require_http_methods(["POST"])
1103 @require_http_methods(["POST", "GET"])
1104 @signed_terms_required
1106 def group_join(request, group_id):
1107 m = Membership(group_id=group_id,
1108 person=request.user,
1109 date_requested=datetime.now())
1112 post_save_redirect = reverse(
1114 kwargs=dict(group_id=group_id))
1115 return HttpResponseRedirect(post_save_redirect)
1116 except IntegrityError, e:
1118 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1119 messages.error(request, msg)
1120 return group_search(request)
1123 @require_http_methods(["POST"])
1124 @signed_terms_required
1126 def group_leave(request, group_id):
1128 m = Membership.objects.select_related().get(
1130 person=request.user)
1131 except Membership.DoesNotExist:
1132 return HttpResponseBadRequest(_(astakos_messages.NOT_A_MEMBER))
1133 if request.user in m.group.owner.all():
1134 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1135 return delete_object(
1139 template_name='im/astakosgroup_list.html',
1140 post_delete_redirect=reverse(
1142 kwargs=dict(group_id=group_id)))
1145 def handle_membership(func):
1147 def wrapper(request, group_id, user_id):
1149 m = Membership.objects.select_related().get(
1152 except Membership.DoesNotExist:
1153 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1155 if request.user not in m.group.owner.all():
1156 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1158 return group_detail(request, group_id)
1162 #@require_http_methods(["POST"])
1163 @require_http_methods(["POST", "GET"])
1164 @signed_terms_required
1167 def approve_member(request, membership):
1169 membership.approve()
1170 realname = membership.person.realname
1171 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1172 messages.success(request, msg)
1173 except AssertionError:
1174 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1175 messages.error(request, msg)
1176 except BaseException, e:
1178 realname = membership.person.realname
1179 msg = _(astakos_messages.GENERIC_ERROR)
1180 messages.error(request, msg)
1183 @signed_terms_required
1186 def disapprove_member(request, membership):
1188 membership.disapprove()
1189 realname = membership.person.realname
1190 msg = MEMBER_REMOVED % realname
1191 messages.success(request, msg)
1192 except BaseException, e:
1194 msg = _(astakos_messages.GENERIC_ERROR)
1195 messages.error(request, msg)
1198 #@require_http_methods(["GET"])
1199 @require_http_methods(["POST", "GET"])
1200 @signed_terms_required
1202 def resource_list(request):
1203 # if request.method == 'POST':
1204 # form = PickResourceForm(request.POST)
1205 # if form.is_valid():
1206 # r = form.cleaned_data.get('resource')
1208 # groups = request.user.membership_set.only('group').filter(
1209 # date_joined__isnull=False)
1210 # groups = [g.group_id for g in groups]
1211 # q = AstakosGroupQuota.objects.select_related().filter(
1212 # resource=r, group__in=groups)
1214 # form = PickResourceForm()
1215 # q = AstakosGroupQuota.objects.none()
1217 # return object_list(request, q,
1218 # template_name='im/astakosuserquota_list.html',
1219 # extra_context={'form': form, 'data':data})
1221 def with_class(entry):
1222 entry['load_class'] = 'red'
1223 max_value = float(entry['maxValue'])
1224 curr_value = float(entry['currValue'])
1226 entry['ratio'] = (curr_value / max_value) * 100
1229 if entry['ratio'] < 66:
1230 entry['load_class'] = 'yellow'
1231 if entry['ratio'] < 33:
1232 entry['load_class'] = 'green'
1235 def pluralize(entry):
1236 entry['plural'] = engine.plural(entry.get('name'))
1239 result = callpoint.get_user_status(request.user.id)
1240 if result.is_success:
1241 backenddata = map(with_class, result.data)
1242 data = map(pluralize, result.data)
1245 messages.error(request, result.reason)
1246 return render_response('im/resource_list.html',
1248 resource_presentation=resource_presentation,
1249 context_instance=get_context(request))
1252 def group_create_list(request):
1253 form = PickResourceForm()
1254 return render_response(
1255 template='im/astakosgroup_create_list.html',
1256 context_instance=get_context(request),)
1259 #@require_http_methods(["GET"])
1260 @require_http_methods(["POST", "GET"])
1261 @signed_terms_required
1263 def billing(request):
1265 today = datetime.today()
1266 month_last_day = calendar.monthrange(today.year, today.month)[1]
1267 data['resources'] = map(with_class, data['resources'])
1268 start = request.POST.get('datefrom', None)
1270 today = datetime.fromtimestamp(int(start))
1271 month_last_day = calendar.monthrange(today.year, today.month)[1]
1273 start = datetime(today.year, today.month, 1).strftime("%s")
1274 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1275 r = request_billing.apply(args=('pgerakios@grnet.gr',
1281 status, data = r.result
1282 data = _clear_billing_data(data)
1284 messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1286 messages.error(request, r.result)
1290 return render_response(
1291 template='im/billing.html',
1292 context_instance=get_context(request),
1294 zerodate=datetime(month=1, year=1970, day=1),
1297 month_last_day=month_last_day)
1300 def _clear_billing_data(data):
1302 # remove addcredits entries
1304 return e['serviceName'] != "addcredits"
1307 def servicefilter(service_name):
1308 service = service_name
1311 return e['serviceName'] == service
1314 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1315 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1316 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1317 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1322 #@require_http_methods(["GET"])
1323 @require_http_methods(["POST", "GET"])
1324 @signed_terms_required
1326 def timeline(request):
1327 # data = {'entity':request.user.email}
1329 timeline_header = ()
1330 # form = TimelineForm(data)
1331 form = TimelineForm()
1332 if request.method == 'POST':
1334 form = TimelineForm(data)
1336 data = form.cleaned_data
1337 timeline_header = ('entity', 'resource',
1338 'event name', 'event date',
1339 'incremental cost', 'total cost')
1340 timeline_body = timeline_charge(
1341 data['entity'], data['resource'],
1342 data['start_date'], data['end_date'],
1343 data['details'], data['operation'])
1345 return render_response(template='im/timeline.html',
1346 context_instance=get_context(request),
1348 timeline_header=timeline_header,
1349 timeline_body=timeline_body)