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':'Diskspace used'
688 'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
689 'is_abbreviation':True,
690 'report_desc':'Number of Virtual Machines'
692 'cyclades.disksize': {
693 'help_text':'resource cyclades.disksize help text',
694 'is_abbreviation':False,
695 'report_desc':'Amount of Disksize used'
698 'help_text':'resource cyclades.disk help text',
699 'is_abbreviation':False,
700 'report_desc':'Amount of Disk used'
703 'help_text':'resource cyclades.ram help text',
704 'is_abbreviation':True,
705 'report_desc':'RAM used'
708 'help_text':'resource cyclades.cpu help text',
709 'is_abbreviation':True,
710 'report_desc':'CPUs used'
712 'cyclades.network.private': {
713 'help_text':'resource cyclades.network.private help text',
714 'is_abbreviation':False,
715 'report_desc':'Network used'
719 @require_http_methods(["GET", "POST"])
720 @signed_terms_required
722 def group_add(request, kind_name='default'):
723 result = callpoint.list_resources()
724 resource_catalog = {'resources':defaultdict(defaultdict),
725 'groups':defaultdict(list)}
726 if result.is_success:
727 for r in result.data:
728 service = r.get('service', '')
729 name = r.get('name', '')
730 group = r.get('group', '')
731 unit = r.get('unit', '')
732 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
733 resource_catalog['resources'][fullname] = dict(unit=unit)
734 resource_catalog['groups'][group].append(fullname)
736 resource_catalog = dict(resource_catalog)
737 for k, v in resource_catalog.iteritems():
738 resource_catalog[k] = dict(v)
742 'Unable to retrieve system resources: %s' % result.reason
746 kind = GroupKind.objects.get(name=kind_name)
748 return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
752 post_save_redirect = '/im/group/%(id)s/'
753 context_processors = None
754 model, form_class = get_model_and_form_class(
756 form_class=AstakosGroupCreationForm
759 if request.method == 'POST':
760 form = form_class(request.POST, request.FILES)
762 return render_response(
763 template='im/astakosgroup_form_summary.html',
764 context_instance=get_context(request),
765 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
766 policies = form.policies(),
767 resource_catalog=resource_catalog,
768 resource_presentation=resource_presentation
775 form = form_class(data)
777 # Create the template, context, response
778 template_name = "%s/%s_form.html" % (
779 model._meta.app_label,
780 model._meta.object_name.lower()
782 t = template_loader.get_template(template_name)
783 c = RequestContext(request, {
786 'resource_catalog':resource_catalog,
787 'resource_presentation':resource_presentation,
788 }, context_processors)
789 return HttpResponse(t.render(c))
792 #@require_http_methods(["POST"])
793 @require_http_methods(["GET", "POST"])
794 @signed_terms_required
796 def group_add_complete(request):
798 form = AstakosGroupCreationSummaryForm(request.POST)
800 d = form.cleaned_data
801 d['owners'] = [request.user]
802 result = callpoint.create_groups((d,)).next()
803 if result.is_success:
804 new_object = result.data[0]
805 msg = _(astakos_messages.OBJECT_CREATED) %\
806 {"verbose_name": model._meta.verbose_name}
807 messages.success(request, msg, fail_silently=True)
811 send_group_creation_notification(
812 template_name='im/group_creation_notification.txt',
815 'owner': request.user,
816 'policies': d.get('policies', [])
819 except SendNotificationError, e:
820 messages.error(request, e, fail_silently=True)
821 post_save_redirect = '/im/group/%(id)s/'
822 return HttpResponseRedirect(post_save_redirect % new_object)
824 msg = _(astakos_messages.OBJECT_CREATED_FAILED) %\
825 {"verbose_name": model._meta.verbose_name,
826 "reason":result.reason}
827 messages.error(request, msg, fail_silently=True)
828 return render_response(
829 template='im/astakosgroup_form_summary.html',
830 context_instance=get_context(request),
834 #@require_http_methods(["GET"])
835 @require_http_methods(["GET", "POST"])
836 @signed_terms_required
838 def group_list(request):
839 none = request.user.astakos_groups.none()
840 q = AstakosGroup.objects.raw("""
841 SELECT auth_group.id,
843 im_groupkind.name AS kindname,
845 owner.email AS groupowner,
846 (SELECT COUNT(*) FROM im_membership
847 WHERE group_id = im_astakosgroup.group_ptr_id
848 AND date_joined IS NOT NULL) AS approved_members_num,
850 SELECT date_joined FROM im_membership
851 WHERE group_id = im_astakosgroup.group_ptr_id
852 AND person_id = %s) IS NULL
853 THEN 0 ELSE 1 END) AS membership_status
855 INNER JOIN im_membership ON (
856 im_astakosgroup.group_ptr_id = im_membership.group_id)
857 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
858 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
859 LEFT JOIN im_astakosuser_owner ON (
860 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
861 LEFT JOIN auth_user as owner ON (
862 im_astakosuser_owner.astakosuser_id = owner.id)
863 WHERE im_membership.person_id = %s
864 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
865 d = defaultdict(list)
867 if request.user.email == g.groupowner:
873 fields = ('own', 'other')
875 v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
877 form = AstakosGroupSortForm({'sort_by': v})
878 if not form.is_valid():
879 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
880 return object_list(request, queryset=none,
881 extra_context={'is_search': False,
884 'own_sorting': own_sorting,
885 'other_sorting': other_sorting,
886 'own_page': request.GET.get('own_page', 1),
887 'other_page': request.GET.get('other_page', 1)
891 @require_http_methods(["GET", "POST"])
892 @signed_terms_required
894 def group_detail(request, group_id):
895 q = AstakosGroup.objects.select_related().filter(pk=group_id)
897 'is_member': """SELECT CASE WHEN EXISTS(
898 SELECT id FROM im_membership
899 WHERE group_id = im_astakosgroup.group_ptr_id
901 THEN 1 ELSE 0 END""" % request.user.id,
902 'is_owner': """SELECT CASE WHEN EXISTS(
903 SELECT id FROM im_astakosuser_owner
904 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
905 AND astakosuser_id = %s)
906 THEN 1 ELSE 0 END""" % request.user.id,
907 'kindname': """SELECT name FROM im_groupkind
908 WHERE id = im_astakosgroup.kind_id"""})
911 context_processors = None
915 except AstakosGroup.DoesNotExist:
916 raise Http404("No %s found matching the query" % (
917 model._meta.verbose_name))
919 update_form = AstakosGroupUpdateForm(instance=obj)
920 addmembers_form = AddGroupMembersForm()
921 if request.method == 'POST':
924 for k, v in request.POST.iteritems():
925 if k in update_form.fields:
927 if k in addmembers_form.fields:
928 addmembers_data[k] = v
929 update_data = update_data or None
930 addmembers_data = addmembers_data or None
931 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
932 addmembers_form = AddGroupMembersForm(addmembers_data)
933 if update_form.is_valid():
935 if addmembers_form.is_valid():
937 map(obj.approve_member, addmembers_form.valid_users)
938 except AssertionError:
939 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
940 messages.error(request, msg)
941 addmembers_form = AddGroupMembersForm()
943 template_name = "%s/%s_detail.html" % (
944 model._meta.app_label, model._meta.object_name.lower())
945 t = template_loader.get_template(template_name)
946 c = RequestContext(request, {
948 }, context_processors)
951 sorting = request.GET.get('sorting')
953 form = MembersSortForm({'sort_by': sorting})
955 sorting = form.cleaned_data.get('sort_by')
957 extra_context = {'update_form': update_form,
958 'addmembers_form': addmembers_form,
959 'page': request.GET.get('page', 1),
961 for key, value in extra_context.items():
966 response = HttpResponse(t.render(c), mimetype=mimetype)
968 request, response, model, getattr(obj, obj._meta.pk.name))
972 @require_http_methods(["GET", "POST"])
973 @signed_terms_required
975 def group_search(request, extra_context=None, **kwargs):
976 q = request.GET.get('q')
977 sorting = request.GET.get('sorting')
978 if request.method == 'GET':
979 form = AstakosGroupSearchForm({'q': q} if q else None)
981 form = AstakosGroupSearchForm(get_query(request))
983 q = form.cleaned_data['q'].strip()
985 queryset = AstakosGroup.objects.select_related()
986 queryset = queryset.filter(name__contains=q)
987 queryset = queryset.filter(approval_date__isnull=False)
988 queryset = queryset.extra(select={
989 'groupname': DB_REPLACE_GROUP_SCHEME,
990 'kindname': "im_groupkind.name",
991 'approved_members_num': """
992 SELECT COUNT(*) FROM im_membership
993 WHERE group_id = im_astakosgroup.group_ptr_id
994 AND date_joined IS NOT NULL""",
995 'membership_approval_date': """
996 SELECT date_joined FROM im_membership
997 WHERE group_id = im_astakosgroup.group_ptr_id
998 AND person_id = %s""" % request.user.id,
1000 SELECT CASE WHEN EXISTS(
1001 SELECT date_joined FROM im_membership
1002 WHERE group_id = im_astakosgroup.group_ptr_id
1004 THEN 1 ELSE 0 END""" % request.user.id,
1006 SELECT CASE WHEN EXISTS(
1007 SELECT id FROM im_astakosuser_owner
1008 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1009 AND astakosuser_id = %s)
1010 THEN 1 ELSE 0 END""" % request.user.id})
1012 # TODO check sorting value
1013 queryset = queryset.order_by(sorting)
1015 queryset = AstakosGroup.objects.none()
1019 paginate_by=PAGINATE_BY,
1020 page=request.GET.get('page') or 1,
1021 template_name='im/astakosgroup_list.html',
1022 extra_context=dict(form=form,
1028 @require_http_methods(["GET", "POST"])
1029 @signed_terms_required
1031 def group_all(request, extra_context=None, **kwargs):
1032 q = AstakosGroup.objects.select_related()
1033 q = q.filter(approval_date__isnull=False)
1034 q = q.extra(select={
1035 'groupname': DB_REPLACE_GROUP_SCHEME,
1036 'kindname': "im_groupkind.name",
1037 'approved_members_num': """
1038 SELECT COUNT(*) FROM im_membership
1039 WHERE group_id = im_astakosgroup.group_ptr_id
1040 AND date_joined IS NOT NULL""",
1041 'membership_approval_date': """
1042 SELECT date_joined FROM im_membership
1043 WHERE group_id = im_astakosgroup.group_ptr_id
1044 AND person_id = %s""" % request.user.id,
1046 SELECT CASE WHEN EXISTS(
1047 SELECT date_joined FROM im_membership
1048 WHERE group_id = im_astakosgroup.group_ptr_id
1050 THEN 1 ELSE 0 END""" % request.user.id})
1051 sorting = request.GET.get('sorting')
1053 # TODO check sorting value
1054 q = q.order_by(sorting)
1058 paginate_by=PAGINATE_BY,
1059 page=request.GET.get('page') or 1,
1060 template_name='im/astakosgroup_list.html',
1061 extra_context=dict(form=AstakosGroupSearchForm(),
1066 #@require_http_methods(["POST"])
1067 @require_http_methods(["POST", "GET"])
1068 @signed_terms_required
1070 def group_join(request, group_id):
1071 m = Membership(group_id=group_id,
1072 person=request.user,
1073 date_requested=datetime.now())
1076 post_save_redirect = reverse(
1078 kwargs=dict(group_id=group_id))
1079 return HttpResponseRedirect(post_save_redirect)
1080 except IntegrityError, e:
1082 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1083 messages.error(request, msg)
1084 return group_search(request)
1087 @require_http_methods(["POST"])
1088 @signed_terms_required
1090 def group_leave(request, group_id):
1092 m = Membership.objects.select_related().get(
1094 person=request.user)
1095 except Membership.DoesNotExist:
1096 return HttpResponseBadRequest(_(astakos_messages.NOT_A_MEMBER))
1097 if request.user in m.group.owner.all():
1098 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1099 return delete_object(
1103 template_name='im/astakosgroup_list.html',
1104 post_delete_redirect=reverse(
1106 kwargs=dict(group_id=group_id)))
1109 def handle_membership(func):
1111 def wrapper(request, group_id, user_id):
1113 m = Membership.objects.select_related().get(
1116 except Membership.DoesNotExist:
1117 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1119 if request.user not in m.group.owner.all():
1120 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1122 return group_detail(request, group_id)
1126 #@require_http_methods(["POST"])
1127 @require_http_methods(["POST", "GET"])
1128 @signed_terms_required
1131 def approve_member(request, membership):
1133 membership.approve()
1134 realname = membership.person.realname
1135 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1136 messages.success(request, msg)
1137 except AssertionError:
1138 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1139 messages.error(request, msg)
1140 except BaseException, e:
1142 realname = membership.person.realname
1143 msg = _(astakos_messages.GENERIC_ERROR)
1144 messages.error(request, msg)
1147 @signed_terms_required
1150 def disapprove_member(request, membership):
1152 membership.disapprove()
1153 realname = membership.person.realname
1154 msg = MEMBER_REMOVED % realname
1155 messages.success(request, msg)
1156 except BaseException, e:
1158 msg = _(astakos_messages.GENERIC_ERROR)
1159 messages.error(request, msg)
1162 #@require_http_methods(["GET"])
1163 @require_http_methods(["POST", "GET"])
1164 @signed_terms_required
1166 def resource_list(request):
1167 # if request.method == 'POST':
1168 # form = PickResourceForm(request.POST)
1169 # if form.is_valid():
1170 # r = form.cleaned_data.get('resource')
1172 # groups = request.user.membership_set.only('group').filter(
1173 # date_joined__isnull=False)
1174 # groups = [g.group_id for g in groups]
1175 # q = AstakosGroupQuota.objects.select_related().filter(
1176 # resource=r, group__in=groups)
1178 # form = PickResourceForm()
1179 # q = AstakosGroupQuota.objects.none()
1181 # return object_list(request, q,
1182 # template_name='im/astakosuserquota_list.html',
1183 # extra_context={'form': form, 'data':data})
1185 def with_class(entry):
1186 entry['load_class'] = 'red'
1187 max_value = float(entry['maxValue'])
1188 curr_value = float(entry['currValue'])
1190 entry['ratio'] = (curr_value / max_value) * 100
1193 if entry['ratio'] < 66:
1194 entry['load_class'] = 'yellow'
1195 if entry['ratio'] < 33:
1196 entry['load_class'] = 'green'
1199 def pluralize(entry):
1200 entry['plural'] = engine.plural(entry.get('name'))
1203 result = callpoint.get_user_status(request.user.id)
1204 if result.is_success:
1205 backenddata = map(with_class, result.data)
1206 data = map(pluralize, result.data)
1209 messages.error(request, result.reason)
1210 return render_response('im/resource_list.html',
1212 resource_presentation=resource_presentation,
1213 context_instance=get_context(request))
1216 def group_create_list(request):
1217 form = PickResourceForm()
1218 return render_response(
1219 template='im/astakosgroup_create_list.html',
1220 context_instance=get_context(request),)
1223 #@require_http_methods(["GET"])
1224 @require_http_methods(["POST", "GET"])
1225 @signed_terms_required
1227 def billing(request):
1229 today = datetime.today()
1230 month_last_day = calendar.monthrange(today.year, today.month)[1]
1231 data['resources'] = map(with_class, data['resources'])
1232 start = request.POST.get('datefrom', None)
1234 today = datetime.fromtimestamp(int(start))
1235 month_last_day = calendar.monthrange(today.year, today.month)[1]
1237 start = datetime(today.year, today.month, 1).strftime("%s")
1238 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1239 r = request_billing.apply(args=('pgerakios@grnet.gr',
1245 status, data = r.result
1246 data = _clear_billing_data(data)
1248 messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1250 messages.error(request, r.result)
1254 return render_response(
1255 template='im/billing.html',
1256 context_instance=get_context(request),
1258 zerodate=datetime(month=1, year=1970, day=1),
1261 month_last_day=month_last_day)
1264 def _clear_billing_data(data):
1266 # remove addcredits entries
1268 return e['serviceName'] != "addcredits"
1271 def servicefilter(service_name):
1272 service = service_name
1275 return e['serviceName'] == service
1278 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1279 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1280 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1281 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1286 #@require_http_methods(["GET"])
1287 @require_http_methods(["POST", "GET"])
1288 @signed_terms_required
1290 def timeline(request):
1291 # data = {'entity':request.user.email}
1293 timeline_header = ()
1294 # form = TimelineForm(data)
1295 form = TimelineForm()
1296 if request.method == 'POST':
1298 form = TimelineForm(data)
1300 data = form.cleaned_data
1301 timeline_header = ('entity', 'resource',
1302 'event name', 'event date',
1303 'incremental cost', 'total cost')
1304 timeline_body = timeline_charge(
1305 data['entity'], data['resource'],
1306 data['start_date'], data['end_date'],
1307 data['details'], data['operation'])
1309 return render_response(template='im/timeline.html',
1310 context_instance=get_context(request),
1312 timeline_header=timeline_header,
1313 timeline_body=timeline_body)