1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
38 engine = inflect.engine()
40 from urllib import quote
41 from functools import wraps
42 from datetime import datetime, timedelta
43 from collections import defaultdict
45 from django.contrib import messages
46 from django.contrib.auth.decorators import login_required
47 from django.contrib.auth.views import password_change
48 from django.core.urlresolvers import reverse
49 from django.db import transaction
50 from django.db.models import Q
51 from django.db.utils import IntegrityError
52 from django.forms.fields import URLField
53 from django.http import (HttpResponse, HttpResponseBadRequest,
54 HttpResponseForbidden, HttpResponseRedirect,
55 HttpResponseBadRequest, Http404)
56 from django.shortcuts import redirect
57 from django.template import RequestContext, loader as template_loader
58 from django.utils.http import urlencode
59 from django.utils.translation import ugettext as _
60 from django.views.generic.create_update import (create_object, delete_object,
61 get_model_and_form_class)
62 from django.views.generic.list_detail import object_list, object_detail
63 from django.http import HttpResponseBadRequest
64 from django.core.xheaders import populate_xheaders
66 from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
67 Resource, EmailChange, GroupKind, Membership,
68 AstakosGroupQuota, RESOURCE_SEPARATOR)
69 from django.views.decorators.http import require_http_methods
71 from astakos.im.activation_backends import get_backend, SimpleBackend
72 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
73 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
74 FeedbackForm, SignApprovalTermsForm,
75 ExtendedPasswordChangeForm, EmailChangeForm,
76 AstakosGroupCreationForm, AstakosGroupSearchForm,
77 AstakosGroupUpdateForm, AddGroupMembersForm,
78 AstakosGroupSortForm, MembersSortForm,
79 TimelineForm, PickResourceForm,
80 AstakosGroupCreationSummaryForm)
81 from astakos.im.functions import (send_feedback, SendMailError,
82 logout as auth_logout,
83 activate as activate_func,
84 switch_account_to_shibboleth,
85 send_group_creation_notification,
86 SendNotificationError)
87 from astakos.im.endpoints.quotaholder import timeline_charge
88 from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
89 LOGGING_LEVEL, PAGINATE_BY)
90 from astakos.im.tasks import request_billing
91 from astakos.im.api.callpoint import AstakosCallpoint
93 import astakos.im.messages as astakos_messages
95 logger = logging.getLogger(__name__)
98 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
101 callpoint = AstakosCallpoint()
103 def render_response(template, tab=None, status=200, reset_cookie=False,
104 context_instance=None, **kwargs):
106 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
107 keyword argument and returns an ``django.http.HttpResponse`` with the
108 specified ``status``.
111 tab = template.partition('_')[0].partition('.html')[0]
112 kwargs.setdefault('tab', tab)
113 html = template_loader.render_to_string(
114 template, kwargs, context_instance=context_instance)
115 response = HttpResponse(html, status=status)
117 set_cookie(response, context_instance['request'].user)
121 def requires_anonymous(func):
123 Decorator checkes whether the request.user is not Anonymous and in that case
124 redirects to `logout`.
127 def wrapper(request, *args):
128 if not request.user.is_anonymous():
129 next = urlencode({'next': request.build_absolute_uri()})
130 logout_uri = reverse(logout) + '?' + next
131 return HttpResponseRedirect(logout_uri)
132 return func(request, *args)
136 def signed_terms_required(func):
138 Decorator checkes whether the request.user is Anonymous and in that case
139 redirects to `logout`.
142 def wrapper(request, *args, **kwargs):
143 if request.user.is_authenticated() and not request.user.signed_terms:
144 params = urlencode({'next': request.build_absolute_uri(),
146 terms_uri = reverse('latest_terms') + '?' + params
147 return HttpResponseRedirect(terms_uri)
148 return func(request, *args, **kwargs)
152 @require_http_methods(["GET", "POST"])
153 @signed_terms_required
154 def index(request, login_template_name='im/login.html', extra_context=None):
156 If there is logged on user renders the profile page otherwise renders login page.
160 ``login_template_name``
161 A custom login template to use. This is optional; if not specified,
162 this will default to ``im/login.html``.
164 ``profile_template_name``
165 A custom profile template to use. This is optional; if not specified,
166 this will default to ``im/profile.html``.
169 An dictionary of variables to add to the template context.
173 im/profile.html or im/login.html or ``template_name`` keyword argument.
176 template_name = login_template_name
177 if request.user.is_authenticated():
178 return HttpResponseRedirect(reverse('edit_profile'))
179 return render_response(template_name,
180 login_form=LoginForm(request=request),
181 context_instance=get_context(request, extra_context))
184 @require_http_methods(["GET", "POST"])
186 @signed_terms_required
187 @transaction.commit_manually
188 def invite(request, template_name='im/invitations.html', extra_context=None):
190 Allows a user to invite somebody else.
192 In case of GET request renders a form for providing the invitee information.
193 In case of POST checks whether the user has not run out of invitations and then
194 sends an invitation email to singup to the service.
196 The view uses commit_manually decorator in order to ensure the number of the
197 user invitations is going to be updated only if the email has been successfully sent.
199 If the user isn't logged in, redirects to settings.LOGIN_URL.
204 A custom template to use. This is optional; if not specified,
205 this will default to ``im/invitations.html``.
208 An dictionary of variables to add to the template context.
212 im/invitations.html or ``template_name`` keyword argument.
216 The view expectes the following settings are defined:
218 * LOGIN_URL: login uri
219 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
223 form = InvitationForm()
225 inviter = request.user
226 if request.method == 'POST':
227 form = InvitationForm(request.POST)
228 if inviter.invitations > 0:
231 email = form.cleaned_data.get('username')
232 realname = form.cleaned_data.get('realname')
233 inviter.invite(email, realname)
234 message = _(astakos_messages.INVITATION_SENT) % locals()
235 messages.success(request, message)
236 except SendMailError, e:
238 messages.error(request, message)
239 transaction.rollback()
240 except BaseException, e:
241 message = _(astakos_messages.GENERIC_ERROR)
242 messages.error(request, message)
244 transaction.rollback()
248 message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
249 messages.error(request, message)
251 sent = [{'email': inv.username,
252 'realname': inv.realname,
253 'is_consumed': inv.is_consumed}
254 for inv in request.user.invitations_sent.all()]
255 kwargs = {'inviter': inviter,
257 context = get_context(request, extra_context, **kwargs)
258 return render_response(template_name,
259 invitation_form=form,
260 context_instance=context)
263 @require_http_methods(["GET", "POST"])
265 @signed_terms_required
266 def edit_profile(request, template_name='im/profile.html', extra_context=None):
268 Allows a user to edit his/her profile.
270 In case of GET request renders a form for displaying the user information.
271 In case of POST updates the user informantion and redirects to ``next``
272 url parameter if exists.
274 If the user isn't logged in, redirects to settings.LOGIN_URL.
279 A custom template to use. This is optional; if not specified,
280 this will default to ``im/profile.html``.
283 An dictionary of variables to add to the template context.
287 im/profile.html or ``template_name`` keyword argument.
291 The view expectes the following settings are defined:
293 * LOGIN_URL: login uri
295 extra_context = extra_context or {}
296 form = ProfileForm(instance=request.user)
297 extra_context['next'] = request.GET.get('next')
299 if request.method == 'POST':
300 form = ProfileForm(request.POST, instance=request.user)
303 prev_token = request.user.auth_token
305 reset_cookie = user.auth_token != prev_token
306 form = ProfileForm(instance=user)
307 next = request.POST.get('next')
309 return redirect(next)
310 msg = _(astakos_messages.PROFILE_UPDATED)
311 messages.success(request, msg)
312 except ValueError, ve:
313 messages.success(request, ve)
314 elif request.method == "GET":
315 if not request.user.is_verified:
316 request.user.is_verified = True
318 return render_response(template_name,
319 reset_cookie=reset_cookie,
321 context_instance=get_context(request,
325 @transaction.commit_manually
326 @require_http_methods(["GET", "POST"])
327 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
329 Allows a user to create a local account.
331 In case of GET request renders a form for entering the user information.
332 In case of POST handles the signup.
334 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
335 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
336 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
337 (see activation_backends);
339 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
340 otherwise renders the same page with a success message.
342 On unsuccessful creation, renders ``template_name`` with an error message.
347 A custom template to render. This is optional;
348 if not specified, this will default to ``im/signup.html``.
351 A custom template to render in case of success. This is optional;
352 if not specified, this will default to ``im/signup_complete.html``.
355 An dictionary of variables to add to the template context.
359 im/signup.html or ``template_name`` keyword argument.
360 im/signup_complete.html or ``on_success`` keyword argument.
362 if request.user.is_authenticated():
363 return HttpResponseRedirect(reverse('edit_profile'))
365 provider = get_query(request).get('provider', 'local')
368 backend = get_backend(request)
369 form = backend.get_signup_form(provider)
371 form = SimpleBackend(request).get_signup_form(provider)
372 messages.error(request, e)
373 if request.method == 'POST':
375 user = form.save(commit=False)
377 result = backend.handle_activation(user)
378 status = messages.SUCCESS
379 message = result.message
381 if 'additional_email' in form.cleaned_data:
382 additional_email = form.cleaned_data['additional_email']
383 if additional_email != user.email:
384 user.additionalmail_set.create(email=additional_email)
385 msg = 'Additional email: %s saved for user %s.' % (
386 additional_email, user.email)
387 logger.log(LOGGING_LEVEL, msg)
388 if user and user.is_active:
389 next = request.POST.get('next', '')
390 response = prepare_response(request, user, next=next)
393 messages.add_message(request, status, message)
395 return render_response(on_success,
396 context_instance=get_context(request, extra_context))
397 except SendMailError, e:
399 messages.error(request, message)
400 transaction.rollback()
401 except BaseException, e:
402 message = _(astakos_messages.GENERIC_ERROR)
403 messages.error(request, message)
405 transaction.rollback()
406 return render_response(template_name,
409 context_instance=get_context(request, extra_context))
412 @require_http_methods(["GET", "POST"])
414 @signed_terms_required
415 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
417 Allows a user to send feedback.
419 In case of GET request renders a form for providing the feedback information.
420 In case of POST sends an email to support team.
422 If the user isn't logged in, redirects to settings.LOGIN_URL.
427 A custom template to use. This is optional; if not specified,
428 this will default to ``im/feedback.html``.
431 An dictionary of variables to add to the template context.
435 im/signup.html or ``template_name`` keyword argument.
439 * LOGIN_URL: login uri
440 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
442 if request.method == 'GET':
443 form = FeedbackForm()
444 if request.method == 'POST':
446 return HttpResponse('Unauthorized', status=401)
448 form = FeedbackForm(request.POST)
450 msg = form.cleaned_data['feedback_msg']
451 data = form.cleaned_data['feedback_data']
453 send_feedback(msg, data, request.user, email_template_name)
454 except SendMailError, e:
455 messages.error(request, message)
457 message = _(astakos_messages.FEEDBACK_SENT)
458 messages.success(request, message)
459 return render_response(template_name,
461 context_instance=get_context(request, extra_context))
464 @require_http_methods(["GET", "POST"])
465 @signed_terms_required
466 def logout(request, template='registration/logged_out.html', extra_context=None):
468 Wraps `django.contrib.auth.logout` and delete the cookie.
470 response = HttpResponse()
471 if request.user.is_authenticated():
472 email = request.user.email
474 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
475 msg = 'Cookie deleted for %s' % email
476 logger.log(LOGGING_LEVEL, msg)
477 next = request.GET.get('next')
479 response['Location'] = next
480 response.status_code = 302
483 response['Location'] = LOGOUT_NEXT
484 response.status_code = 301
486 messages.success(request, _(astakos_messages.LOGOUT_SUCCESS))
487 context = get_context(request, extra_context)
489 template_loader.render_to_string(template, context_instance=context))
493 @require_http_methods(["GET", "POST"])
494 @transaction.commit_manually
495 def activate(request, greeting_email_template_name='im/welcome_email.txt',
496 helpdesk_email_template_name='im/helpdesk_notification.txt'):
498 Activates the user identified by the ``auth`` request parameter, sends a welcome email
499 and renews the user token.
501 The view uses commit_manually decorator in order to ensure the user state will be updated
502 only if the email will be send successfully.
504 token = request.GET.get('auth')
505 next = request.GET.get('next')
507 user = AstakosUser.objects.get(auth_token=token)
508 except AstakosUser.DoesNotExist:
509 return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
512 message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
513 messages.error(request, message)
514 return index(request)
517 local_user = AstakosUser.objects.get(
522 except AstakosUser.DoesNotExist:
526 greeting_email_template_name,
527 helpdesk_email_template_name,
530 response = prepare_response(request, user, next, renew=True)
533 except SendMailError, e:
535 messages.error(request, message)
536 transaction.rollback()
537 return index(request)
538 except BaseException, e:
539 message = _(astakos_messages.GENERIC_ERROR)
540 messages.error(request, message)
542 transaction.rollback()
543 return index(request)
546 user = switch_account_to_shibboleth(
549 greeting_email_template_name
551 response = prepare_response(request, user, next, renew=True)
554 except SendMailError, e:
556 messages.error(request, message)
557 transaction.rollback()
558 return index(request)
559 except BaseException, e:
560 message = _(astakos_messages.GENERIC_ERROR)
561 messages.error(request, message)
563 transaction.rollback()
564 return index(request)
567 @require_http_methods(["GET", "POST"])
568 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
573 term = ApprovalTerms.objects.order_by('-id')[0]
578 term = ApprovalTerms.objects.get(id=term_id)
579 except ApprovalTerms.DoesNotExist, e:
583 messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
584 return HttpResponseRedirect(reverse('index'))
585 f = open(term.location, 'r')
588 if request.method == 'POST':
589 next = request.POST.get('next')
591 next = reverse('index')
592 form = SignApprovalTermsForm(request.POST, instance=request.user)
593 if not form.is_valid():
594 return render_response(template_name,
596 approval_terms_form=form,
597 context_instance=get_context(request, extra_context))
599 return HttpResponseRedirect(next)
602 if request.user.is_authenticated() and not request.user.signed_terms:
603 form = SignApprovalTermsForm(instance=request.user)
604 return render_response(template_name,
606 approval_terms_form=form,
607 context_instance=get_context(request, extra_context))
610 @require_http_methods(["GET", "POST"])
611 @signed_terms_required
612 def change_password(request):
613 return password_change(request,
614 post_change_redirect=reverse('edit_profile'),
615 password_change_form=ExtendedPasswordChangeForm)
618 @require_http_methods(["GET", "POST"])
619 @signed_terms_required
621 @transaction.commit_manually
622 def change_email(request, activation_key=None,
623 email_template_name='registration/email_change_email.txt',
624 form_template_name='registration/email_change_form.html',
625 confirm_template_name='registration/email_change_done.html',
629 user = EmailChange.objects.change_email(activation_key)
630 if request.user.is_authenticated() and request.user == user:
631 msg = _(astakos_messages.EMAIL_CHANGED)
632 messages.success(request, msg)
634 response = prepare_response(request, user)
637 except ValueError, e:
638 messages.error(request, e)
639 return render_response(confirm_template_name,
640 modified_user=user if 'user' in locals(
642 context_instance=get_context(request,
645 if not request.user.is_authenticated():
646 path = quote(request.get_full_path())
647 url = request.build_absolute_uri(reverse('index'))
648 return HttpResponseRedirect(url + '?next=' + path)
649 form = EmailChangeForm(request.POST or None)
650 if request.method == 'POST' and form.is_valid():
652 ec = form.save(email_template_name, request)
653 except SendMailError, e:
655 messages.error(request, msg)
656 transaction.rollback()
657 except IntegrityError, e:
658 msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
659 messages.error(request, msg)
661 msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
662 messages.success(request, msg)
664 return render_response(form_template_name,
666 context_instance=get_context(request,
671 resource_presentation = {
673 'help_text':'group compute help text',
674 'is_abbreviation':False,
678 'help_text':'group storage help text',
679 'is_abbreviation':False,
682 'pithos+.diskspace': {
683 'help_text':'resource pithos+.diskspace help text',
684 'is_abbreviation':False,
685 'report_desc':'Pithos+ Diskspace',
686 'placeholder':'eg. 10GB'
689 'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
690 'is_abbreviation':True,
691 'report_desc':'Virtual Machines',
692 'placeholder':'eg. 2'
694 'cyclades.disksize': {
695 'help_text':'resource cyclades.disksize help text',
696 'is_abbreviation':False,
697 'report_desc':'Disksize',
698 'placeholder':'eg. 5GB'
701 'help_text':'resource cyclades.disk help text',
702 'is_abbreviation':False,
703 'report_desc':'Disk',
704 'placeholder':'eg. 5GB'
707 'help_text':'resource cyclades.ram help text',
708 'is_abbreviation':True,
710 'placeholder':'eg. 4GB'
713 'help_text':'resource cyclades.cpu help text',
714 'is_abbreviation':True,
715 'report_desc':'CPUs',
716 'placeholder':'eg. 1'
718 'cyclades.network.private': {
719 'help_text':'resource cyclades.network.private help text',
720 'is_abbreviation':False,
721 'report_desc':'Network',
722 'placeholder':'eg. 1'
726 @require_http_methods(["GET", "POST"])
727 @signed_terms_required
729 def group_add(request, kind_name='default'):
730 result = callpoint.list_resources()
731 resource_catalog = {'resources':defaultdict(defaultdict),
732 'groups':defaultdict(list)}
733 if result.is_success:
734 for r in result.data:
735 service = r.get('service', '')
736 name = r.get('name', '')
737 group = r.get('group', '')
738 unit = r.get('unit', '')
739 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
740 resource_catalog['resources'][fullname] = dict(unit=unit)
741 resource_catalog['groups'][group].append(fullname)
743 resource_catalog = dict(resource_catalog)
744 for k, v in resource_catalog.iteritems():
745 resource_catalog[k] = dict(v)
749 'Unable to retrieve system resources: %s' % result.reason
753 kind = GroupKind.objects.get(name=kind_name)
755 return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
759 post_save_redirect = '/im/group/%(id)s/'
760 context_processors = None
761 model, form_class = get_model_and_form_class(
763 form_class=AstakosGroupCreationForm
766 if request.method == 'POST':
767 form = form_class(request.POST, request.FILES)
769 return render_response(
770 template='im/astakosgroup_form_summary.html',
771 context_instance=get_context(request),
772 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
773 policies = form.policies(),
774 resource_catalog=resource_catalog,
775 resource_presentation=resource_presentation
782 form = form_class(data)
784 # Create the template, context, response
785 template_name = "%s/%s_form.html" % (
786 model._meta.app_label,
787 model._meta.object_name.lower()
789 t = template_loader.get_template(template_name)
790 c = RequestContext(request, {
793 'resource_catalog':resource_catalog,
794 'resource_presentation':resource_presentation,
795 }, context_processors)
796 return HttpResponse(t.render(c))
799 #@require_http_methods(["POST"])
800 @require_http_methods(["GET", "POST"])
801 @signed_terms_required
803 def group_add_complete(request):
805 form = AstakosGroupCreationSummaryForm(request.POST)
807 d = form.cleaned_data
808 d['owners'] = [request.user]
809 result = callpoint.create_groups((d,)).next()
810 if result.is_success:
811 new_object = result.data[0]
812 msg = _(astakos_messages.OBJECT_CREATED) %\
813 {"verbose_name": model._meta.verbose_name}
814 messages.success(request, msg, fail_silently=True)
818 send_group_creation_notification(
819 template_name='im/group_creation_notification.txt',
822 'owner': request.user,
823 'policies': d.get('policies', [])
826 except SendNotificationError, e:
827 messages.error(request, e, fail_silently=True)
828 post_save_redirect = '/im/group/%(id)s/'
829 return HttpResponseRedirect(post_save_redirect % new_object)
831 msg = _(astakos_messages.OBJECT_CREATED_FAILED) %\
832 {"verbose_name": model._meta.verbose_name,
833 "reason":result.reason}
834 messages.error(request, msg, fail_silently=True)
835 return render_response(
836 template='im/astakosgroup_form_summary.html',
837 context_instance=get_context(request),
841 #@require_http_methods(["GET"])
842 @require_http_methods(["GET", "POST"])
843 @signed_terms_required
845 def group_list(request):
846 none = request.user.astakos_groups.none()
847 q = AstakosGroup.objects.raw("""
848 SELECT auth_group.id,
850 im_groupkind.name AS kindname,
852 owner.email AS groupowner,
853 (SELECT COUNT(*) FROM im_membership
854 WHERE group_id = im_astakosgroup.group_ptr_id
855 AND date_joined IS NOT NULL) AS approved_members_num,
857 SELECT date_joined FROM im_membership
858 WHERE group_id = im_astakosgroup.group_ptr_id
859 AND person_id = %s) IS NULL
860 THEN 0 ELSE 1 END) AS membership_status
862 INNER JOIN im_membership ON (
863 im_astakosgroup.group_ptr_id = im_membership.group_id)
864 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
865 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
866 LEFT JOIN im_astakosuser_owner ON (
867 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
868 LEFT JOIN auth_user as owner ON (
869 im_astakosuser_owner.astakosuser_id = owner.id)
870 WHERE im_membership.person_id = %s
871 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
872 d = defaultdict(list)
875 if request.user.email == g.groupowner:
884 fields = ('own', 'other', 'all')
886 v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
888 form = AstakosGroupSortForm({'sort_by': v})
889 if not form.is_valid():
890 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
891 return object_list(request, queryset=none,
892 extra_context={'is_search': False,
896 'own_sorting': own_sorting,
897 'other_sorting': other_sorting,
898 'all_sorting': all_sorting,
899 'own_page': request.GET.get('own_page', 1),
900 'other_page': request.GET.get('other_page', 1),
901 'all_page': request.GET.get('all_page', 1)
905 @require_http_methods(["GET", "POST"])
906 @signed_terms_required
908 def group_detail(request, group_id):
909 q = AstakosGroup.objects.select_related().filter(pk=group_id)
911 'is_member': """SELECT CASE WHEN EXISTS(
912 SELECT id FROM im_membership
913 WHERE group_id = im_astakosgroup.group_ptr_id
915 THEN 1 ELSE 0 END""" % request.user.id,
916 'is_owner': """SELECT CASE WHEN EXISTS(
917 SELECT id FROM im_astakosuser_owner
918 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
919 AND astakosuser_id = %s)
920 THEN 1 ELSE 0 END""" % request.user.id,
921 'kindname': """SELECT name FROM im_groupkind
922 WHERE id = im_astakosgroup.kind_id"""})
925 context_processors = None
929 except AstakosGroup.DoesNotExist:
930 raise Http404("No %s found matching the query" % (
931 model._meta.verbose_name))
933 update_form = AstakosGroupUpdateForm(instance=obj)
934 addmembers_form = AddGroupMembersForm()
935 if request.method == 'POST':
938 for k, v in request.POST.iteritems():
939 if k in update_form.fields:
941 if k in addmembers_form.fields:
942 addmembers_data[k] = v
943 update_data = update_data or None
944 addmembers_data = addmembers_data or None
945 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
946 addmembers_form = AddGroupMembersForm(addmembers_data)
947 if update_form.is_valid():
949 if addmembers_form.is_valid():
951 map(obj.approve_member, addmembers_form.valid_users)
952 except AssertionError:
953 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
954 messages.error(request, msg)
955 addmembers_form = AddGroupMembersForm()
957 template_name = "%s/%s_detail.html" % (
958 model._meta.app_label, model._meta.object_name.lower())
959 t = template_loader.get_template(template_name)
960 c = RequestContext(request, {
962 }, context_processors)
965 sorting = request.GET.get('sorting')
967 form = MembersSortForm({'sort_by': sorting})
969 sorting = form.cleaned_data.get('sort_by')
971 extra_context = {'update_form': update_form,
972 'addmembers_form': addmembers_form,
973 'page': request.GET.get('page', 1),
975 for key, value in extra_context.items():
980 response = HttpResponse(t.render(c), mimetype=mimetype)
982 request, response, model, getattr(obj, obj._meta.pk.name))
986 @require_http_methods(["GET", "POST"])
987 @signed_terms_required
989 def group_search(request, extra_context=None, **kwargs):
990 q = request.GET.get('q')
991 sorting = request.GET.get('sorting')
992 if request.method == 'GET':
993 form = AstakosGroupSearchForm({'q': q} if q else None)
995 form = AstakosGroupSearchForm(get_query(request))
997 q = form.cleaned_data['q'].strip()
999 queryset = AstakosGroup.objects.select_related()
1000 queryset = queryset.filter(name__contains=q)
1001 queryset = queryset.filter(approval_date__isnull=False)
1002 queryset = queryset.extra(select={
1003 'groupname': DB_REPLACE_GROUP_SCHEME,
1004 'kindname': "im_groupkind.name",
1005 'approved_members_num': """
1006 SELECT COUNT(*) FROM im_membership
1007 WHERE group_id = im_astakosgroup.group_ptr_id
1008 AND date_joined IS NOT NULL""",
1009 'membership_approval_date': """
1010 SELECT date_joined FROM im_membership
1011 WHERE group_id = im_astakosgroup.group_ptr_id
1012 AND person_id = %s""" % request.user.id,
1014 SELECT CASE WHEN EXISTS(
1015 SELECT date_joined FROM im_membership
1016 WHERE group_id = im_astakosgroup.group_ptr_id
1018 THEN 1 ELSE 0 END""" % request.user.id,
1020 SELECT CASE WHEN EXISTS(
1021 SELECT id FROM im_astakosuser_owner
1022 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1023 AND astakosuser_id = %s)
1024 THEN 1 ELSE 0 END""" % request.user.id})
1026 # TODO check sorting value
1027 queryset = queryset.order_by(sorting)
1029 queryset = AstakosGroup.objects.none()
1033 paginate_by=PAGINATE_BY,
1034 page=request.GET.get('page') or 1,
1035 template_name='im/astakosgroup_list.html',
1036 extra_context=dict(form=form,
1042 @require_http_methods(["GET", "POST"])
1043 @signed_terms_required
1045 def group_all(request, extra_context=None, **kwargs):
1046 q = AstakosGroup.objects.select_related()
1047 q = q.filter(approval_date__isnull=False)
1048 q = q.extra(select={
1049 'groupname': DB_REPLACE_GROUP_SCHEME,
1050 'kindname': "im_groupkind.name",
1051 'approved_members_num': """
1052 SELECT COUNT(*) FROM im_membership
1053 WHERE group_id = im_astakosgroup.group_ptr_id
1054 AND date_joined IS NOT NULL""",
1055 'membership_approval_date': """
1056 SELECT date_joined FROM im_membership
1057 WHERE group_id = im_astakosgroup.group_ptr_id
1058 AND person_id = %s""" % request.user.id,
1060 SELECT CASE WHEN EXISTS(
1061 SELECT date_joined FROM im_membership
1062 WHERE group_id = im_astakosgroup.group_ptr_id
1064 THEN 1 ELSE 0 END""" % request.user.id})
1065 sorting = request.GET.get('sorting')
1067 # TODO check sorting value
1068 q = q.order_by(sorting)
1072 paginate_by=PAGINATE_BY,
1073 page=request.GET.get('page') or 1,
1074 template_name='im/astakosgroup_list.html',
1075 extra_context=dict(form=AstakosGroupSearchForm(),
1080 #@require_http_methods(["POST"])
1081 @require_http_methods(["POST", "GET"])
1082 @signed_terms_required
1084 def group_join(request, group_id):
1085 m = Membership(group_id=group_id,
1086 person=request.user,
1087 date_requested=datetime.now())
1090 post_save_redirect = reverse(
1092 kwargs=dict(group_id=group_id))
1093 return HttpResponseRedirect(post_save_redirect)
1094 except IntegrityError, e:
1096 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1097 messages.error(request, msg)
1098 return group_search(request)
1101 @require_http_methods(["POST"])
1102 @signed_terms_required
1104 def group_leave(request, group_id):
1106 m = Membership.objects.select_related().get(
1108 person=request.user)
1109 except Membership.DoesNotExist:
1110 return HttpResponseBadRequest(_(astakos_messages.NOT_A_MEMBER))
1111 if request.user in m.group.owner.all():
1112 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1113 return delete_object(
1117 template_name='im/astakosgroup_list.html',
1118 post_delete_redirect=reverse(
1120 kwargs=dict(group_id=group_id)))
1123 def handle_membership(func):
1125 def wrapper(request, group_id, user_id):
1127 m = Membership.objects.select_related().get(
1130 except Membership.DoesNotExist:
1131 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1133 if request.user not in m.group.owner.all():
1134 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1136 return group_detail(request, group_id)
1140 #@require_http_methods(["POST"])
1141 @require_http_methods(["POST", "GET"])
1142 @signed_terms_required
1145 def approve_member(request, membership):
1147 membership.approve()
1148 realname = membership.person.realname
1149 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1150 messages.success(request, msg)
1151 except AssertionError:
1152 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1153 messages.error(request, msg)
1154 except BaseException, e:
1156 realname = membership.person.realname
1157 msg = _(astakos_messages.GENERIC_ERROR)
1158 messages.error(request, msg)
1161 @signed_terms_required
1164 def disapprove_member(request, membership):
1166 membership.disapprove()
1167 realname = membership.person.realname
1168 msg = MEMBER_REMOVED % realname
1169 messages.success(request, msg)
1170 except BaseException, e:
1172 msg = _(astakos_messages.GENERIC_ERROR)
1173 messages.error(request, msg)
1176 #@require_http_methods(["GET"])
1177 @require_http_methods(["POST", "GET"])
1178 @signed_terms_required
1180 def resource_list(request):
1181 # if request.method == 'POST':
1182 # form = PickResourceForm(request.POST)
1183 # if form.is_valid():
1184 # r = form.cleaned_data.get('resource')
1186 # groups = request.user.membership_set.only('group').filter(
1187 # date_joined__isnull=False)
1188 # groups = [g.group_id for g in groups]
1189 # q = AstakosGroupQuota.objects.select_related().filter(
1190 # resource=r, group__in=groups)
1192 # form = PickResourceForm()
1193 # q = AstakosGroupQuota.objects.none()
1195 # return object_list(request, q,
1196 # template_name='im/astakosuserquota_list.html',
1197 # extra_context={'form': form, 'data':data})
1199 def with_class(entry):
1200 entry['load_class'] = 'red'
1201 max_value = float(entry['maxValue'])
1202 curr_value = float(entry['currValue'])
1204 entry['ratio'] = (curr_value / max_value) * 100
1207 if entry['ratio'] < 66:
1208 entry['load_class'] = 'yellow'
1209 if entry['ratio'] < 33:
1210 entry['load_class'] = 'green'
1213 def pluralize(entry):
1214 entry['plural'] = engine.plural(entry.get('name'))
1217 result = callpoint.get_user_status(request.user.id)
1218 if result.is_success:
1219 backenddata = map(with_class, result.data)
1220 data = map(pluralize, result.data)
1223 messages.error(request, result.reason)
1224 return render_response('im/resource_list.html',
1226 resource_presentation=resource_presentation,
1227 context_instance=get_context(request))
1230 def group_create_list(request):
1231 form = PickResourceForm()
1232 return render_response(
1233 template='im/astakosgroup_create_list.html',
1234 context_instance=get_context(request),)
1237 #@require_http_methods(["GET"])
1238 @require_http_methods(["POST", "GET"])
1239 @signed_terms_required
1241 def billing(request):
1243 today = datetime.today()
1244 month_last_day = calendar.monthrange(today.year, today.month)[1]
1245 data['resources'] = map(with_class, data['resources'])
1246 start = request.POST.get('datefrom', None)
1248 today = datetime.fromtimestamp(int(start))
1249 month_last_day = calendar.monthrange(today.year, today.month)[1]
1251 start = datetime(today.year, today.month, 1).strftime("%s")
1252 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1253 r = request_billing.apply(args=('pgerakios@grnet.gr',
1259 status, data = r.result
1260 data = _clear_billing_data(data)
1262 messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1264 messages.error(request, r.result)
1268 return render_response(
1269 template='im/billing.html',
1270 context_instance=get_context(request),
1272 zerodate=datetime(month=1, year=1970, day=1),
1275 month_last_day=month_last_day)
1278 def _clear_billing_data(data):
1280 # remove addcredits entries
1282 return e['serviceName'] != "addcredits"
1285 def servicefilter(service_name):
1286 service = service_name
1289 return e['serviceName'] == service
1292 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1293 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1294 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1295 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1300 #@require_http_methods(["GET"])
1301 @require_http_methods(["POST", "GET"])
1302 @signed_terms_required
1304 def timeline(request):
1305 # data = {'entity':request.user.email}
1307 timeline_header = ()
1308 # form = TimelineForm(data)
1309 form = TimelineForm()
1310 if request.method == 'POST':
1312 form = TimelineForm(data)
1314 data = form.cleaned_data
1315 timeline_header = ('entity', 'resource',
1316 'event name', 'event date',
1317 'incremental cost', 'total cost')
1318 timeline_body = timeline_charge(
1319 data['entity'], data['resource'],
1320 data['start_date'], data['end_date'],
1321 data['details'], data['operation'])
1323 return render_response(template='im/timeline.html',
1324 context_instance=get_context(request),
1326 timeline_header=timeline_header,
1327 timeline_body=timeline_body)