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 from astakos.im.functions import (send_feedback, SendMailError,
81 logout as auth_logout,
82 activate as activate_func,
83 switch_account_to_shibboleth,
84 send_group_creation_notification,
85 SendNotificationError)
86 from astakos.im.endpoints.quotaholder import timeline_charge
87 from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
88 LOGGING_LEVEL, PAGINATE_BY)
89 from astakos.im.tasks import request_billing
90 from astakos.im.api.callpoint import AstakosCallpoint
92 logger = logging.getLogger(__name__)
95 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
98 callpoint = AstakosCallpoint()
100 def render_response(template, tab=None, status=200, reset_cookie=False,
101 context_instance=None, **kwargs):
103 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
104 keyword argument and returns an ``django.http.HttpResponse`` with the
105 specified ``status``.
108 tab = template.partition('_')[0].partition('.html')[0]
109 kwargs.setdefault('tab', tab)
110 html = template_loader.render_to_string(
111 template, kwargs, context_instance=context_instance)
112 response = HttpResponse(html, status=status)
114 set_cookie(response, context_instance['request'].user)
118 def requires_anonymous(func):
120 Decorator checkes whether the request.user is not Anonymous and in that case
121 redirects to `logout`.
124 def wrapper(request, *args):
125 if not request.user.is_anonymous():
126 next = urlencode({'next': request.build_absolute_uri()})
127 logout_uri = reverse(logout) + '?' + next
128 return HttpResponseRedirect(logout_uri)
129 return func(request, *args)
133 def signed_terms_required(func):
135 Decorator checkes whether the request.user is Anonymous and in that case
136 redirects to `logout`.
139 def wrapper(request, *args, **kwargs):
140 if request.user.is_authenticated() and not request.user.signed_terms:
141 params = urlencode({'next': request.build_absolute_uri(),
143 terms_uri = reverse('latest_terms') + '?' + params
144 return HttpResponseRedirect(terms_uri)
145 return func(request, *args, **kwargs)
149 @require_http_methods(["GET", "POST"])
150 @signed_terms_required
151 def index(request, login_template_name='im/login.html', extra_context=None):
153 If there is logged on user renders the profile page otherwise renders login page.
157 ``login_template_name``
158 A custom login template to use. This is optional; if not specified,
159 this will default to ``im/login.html``.
161 ``profile_template_name``
162 A custom profile template to use. This is optional; if not specified,
163 this will default to ``im/profile.html``.
166 An dictionary of variables to add to the template context.
170 im/profile.html or im/login.html or ``template_name`` keyword argument.
173 template_name = login_template_name
174 if request.user.is_authenticated():
175 return HttpResponseRedirect(reverse('edit_profile'))
176 return render_response(template_name,
177 login_form=LoginForm(request=request),
178 context_instance=get_context(request, extra_context))
181 @require_http_methods(["GET", "POST"])
183 @signed_terms_required
184 @transaction.commit_manually
185 def invite(request, template_name='im/invitations.html', extra_context=None):
187 Allows a user to invite somebody else.
189 In case of GET request renders a form for providing the invitee information.
190 In case of POST checks whether the user has not run out of invitations and then
191 sends an invitation email to singup to the service.
193 The view uses commit_manually decorator in order to ensure the number of the
194 user invitations is going to be updated only if the email has been successfully sent.
196 If the user isn't logged in, redirects to settings.LOGIN_URL.
201 A custom template to use. This is optional; if not specified,
202 this will default to ``im/invitations.html``.
205 An dictionary of variables to add to the template context.
209 im/invitations.html or ``template_name`` keyword argument.
213 The view expectes the following settings are defined:
215 * LOGIN_URL: login uri
216 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
220 form = InvitationForm()
222 inviter = request.user
223 if request.method == 'POST':
224 form = InvitationForm(request.POST)
225 if inviter.invitations > 0:
228 email = form.cleaned_data.get('username')
229 realname = form.cleaned_data.get('realname')
230 inviter.invite(email, realname)
231 message = _('Invitation sent to %s' % email)
232 messages.success(request, message)
233 except SendMailError, e:
235 messages.error(request, message)
236 transaction.rollback()
237 except BaseException, e:
238 message = _('Something went wrong.')
239 messages.error(request, message)
241 transaction.rollback()
245 message = _('No invitations left')
246 messages.error(request, message)
248 sent = [{'email': inv.username,
249 'realname': inv.realname,
250 'is_consumed': inv.is_consumed}
251 for inv in request.user.invitations_sent.all()]
252 kwargs = {'inviter': inviter,
254 context = get_context(request, extra_context, **kwargs)
255 return render_response(template_name,
256 invitation_form=form,
257 context_instance=context)
260 @require_http_methods(["GET", "POST"])
262 @signed_terms_required
263 def edit_profile(request, template_name='im/profile.html', extra_context=None):
265 Allows a user to edit his/her profile.
267 In case of GET request renders a form for displaying the user information.
268 In case of POST updates the user informantion and redirects to ``next``
269 url parameter if exists.
271 If the user isn't logged in, redirects to settings.LOGIN_URL.
276 A custom template to use. This is optional; if not specified,
277 this will default to ``im/profile.html``.
280 An dictionary of variables to add to the template context.
284 im/profile.html or ``template_name`` keyword argument.
288 The view expectes the following settings are defined:
290 * LOGIN_URL: login uri
292 extra_context = extra_context or {}
293 form = ProfileForm(instance=request.user)
294 extra_context['next'] = request.GET.get('next')
296 if request.method == 'POST':
297 form = ProfileForm(request.POST, instance=request.user)
300 prev_token = request.user.auth_token
302 reset_cookie = user.auth_token != prev_token
303 form = ProfileForm(instance=user)
304 next = request.POST.get('next')
306 return redirect(next)
307 msg = _('Profile has been updated successfully')
308 messages.success(request, msg)
309 except ValueError, ve:
310 messages.success(request, ve)
311 elif request.method == "GET":
312 if not request.user.is_verified:
313 request.user.is_verified = True
315 return render_response(template_name,
316 reset_cookie=reset_cookie,
318 context_instance=get_context(request,
322 @transaction.commit_manually
323 @require_http_methods(["GET", "POST"])
324 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
326 Allows a user to create a local account.
328 In case of GET request renders a form for entering the user information.
329 In case of POST handles the signup.
331 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
332 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
333 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
334 (see activation_backends);
336 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
337 otherwise renders the same page with a success message.
339 On unsuccessful creation, renders ``template_name`` with an error message.
344 A custom template to render. This is optional;
345 if not specified, this will default to ``im/signup.html``.
348 A custom template to render in case of success. This is optional;
349 if not specified, this will default to ``im/signup_complete.html``.
352 An dictionary of variables to add to the template context.
356 im/signup.html or ``template_name`` keyword argument.
357 im/signup_complete.html or ``on_success`` keyword argument.
359 if request.user.is_authenticated():
360 return HttpResponseRedirect(reverse('edit_profile'))
362 provider = get_query(request).get('provider', 'local')
365 backend = get_backend(request)
366 form = backend.get_signup_form(provider)
368 form = SimpleBackend(request).get_signup_form(provider)
369 messages.error(request, e)
370 if request.method == 'POST':
372 user = form.save(commit=False)
374 result = backend.handle_activation(user)
375 status = messages.SUCCESS
376 message = result.message
378 if 'additional_email' in form.cleaned_data:
379 additional_email = form.cleaned_data['additional_email']
380 if additional_email != user.email:
381 user.additionalmail_set.create(email=additional_email)
382 msg = 'Additional email: %s saved for user %s.' % (
383 additional_email, user.email)
384 logger.log(LOGGING_LEVEL, msg)
385 if user and user.is_active:
386 next = request.POST.get('next', '')
388 return prepare_response(request, user, next=next)
389 messages.add_message(request, status, message)
391 return render_response(on_success,
392 context_instance=get_context(request, extra_context))
393 except SendMailError, e:
395 messages.error(request, message)
396 transaction.rollback()
397 except BaseException, e:
398 message = _('Something went wrong.')
399 messages.error(request, message)
401 transaction.rollback()
402 return render_response(template_name,
405 context_instance=get_context(request, extra_context))
408 @require_http_methods(["GET", "POST"])
410 @signed_terms_required
411 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
413 Allows a user to send feedback.
415 In case of GET request renders a form for providing the feedback information.
416 In case of POST sends an email to support team.
418 If the user isn't logged in, redirects to settings.LOGIN_URL.
423 A custom template to use. This is optional; if not specified,
424 this will default to ``im/feedback.html``.
427 An dictionary of variables to add to the template context.
431 im/signup.html or ``template_name`` keyword argument.
435 * LOGIN_URL: login uri
436 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
438 if request.method == 'GET':
439 form = FeedbackForm()
440 if request.method == 'POST':
442 return HttpResponse('Unauthorized', status=401)
444 form = FeedbackForm(request.POST)
446 msg = form.cleaned_data['feedback_msg']
447 data = form.cleaned_data['feedback_data']
449 send_feedback(msg, data, request.user, email_template_name)
450 except SendMailError, e:
451 messages.error(request, message)
453 message = _('Feedback successfully sent')
454 messages.success(request, message)
455 return render_response(template_name,
457 context_instance=get_context(request, extra_context))
460 @require_http_methods(["GET", "POST"])
461 @signed_terms_required
462 def logout(request, template='registration/logged_out.html', extra_context=None):
464 Wraps `django.contrib.auth.logout` and delete the cookie.
466 response = HttpResponse()
467 if request.user.is_authenticated():
468 email = request.user.email
470 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
471 msg = 'Cookie deleted for %s' % email
472 logger.log(LOGGING_LEVEL, msg)
473 next = request.GET.get('next')
475 response['Location'] = next
476 response.status_code = 302
479 response['Location'] = LOGOUT_NEXT
480 response.status_code = 301
482 messages.success(request, _('You have successfully logged out.'))
483 context = get_context(request, extra_context)
485 template_loader.render_to_string(template, context_instance=context))
489 @require_http_methods(["GET", "POST"])
490 @transaction.commit_manually
491 def activate(request, greeting_email_template_name='im/welcome_email.txt',
492 helpdesk_email_template_name='im/helpdesk_notification.txt'):
494 Activates the user identified by the ``auth`` request parameter, sends a welcome email
495 and renews the user token.
497 The view uses commit_manually decorator in order to ensure the user state will be updated
498 only if the email will be send successfully.
500 token = request.GET.get('auth')
501 next = request.GET.get('next')
503 user = AstakosUser.objects.get(auth_token=token)
504 except AstakosUser.DoesNotExist:
505 return HttpResponseBadRequest(_('No such user'))
508 message = _('Account already active.')
509 messages.error(request, message)
510 return index(request)
513 local_user = AstakosUser.objects.get(
518 except AstakosUser.DoesNotExist:
522 greeting_email_template_name,
523 helpdesk_email_template_name,
526 response = prepare_response(request, user, next, renew=True)
529 except SendMailError, e:
531 messages.error(request, message)
532 transaction.rollback()
533 return index(request)
534 except BaseException, e:
535 message = _('Something went wrong.')
536 messages.error(request, message)
538 transaction.rollback()
539 return index(request)
542 user = switch_account_to_shibboleth(
545 greeting_email_template_name
547 response = prepare_response(request, user, next, renew=True)
550 except SendMailError, e:
552 messages.error(request, message)
553 transaction.rollback()
554 return index(request)
555 except BaseException, e:
556 message = _('Something went wrong.')
557 messages.error(request, message)
559 transaction.rollback()
560 return index(request)
563 @require_http_methods(["GET", "POST"])
564 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
569 term = ApprovalTerms.objects.order_by('-id')[0]
574 term = ApprovalTerms.objects.get(id=term_id)
575 except ApprovalTerms.DoesNotExist, e:
579 messages.error(request, 'There are no approval terms.')
580 return HttpResponseRedirect(reverse('index'))
581 f = open(term.location, 'r')
584 if request.method == 'POST':
585 next = request.POST.get('next')
587 next = reverse('index')
588 form = SignApprovalTermsForm(request.POST, instance=request.user)
589 if not form.is_valid():
590 return render_response(template_name,
592 approval_terms_form=form,
593 context_instance=get_context(request, extra_context))
595 return HttpResponseRedirect(next)
598 if request.user.is_authenticated() and not request.user.signed_terms:
599 form = SignApprovalTermsForm(instance=request.user)
600 return render_response(template_name,
602 approval_terms_form=form,
603 context_instance=get_context(request, extra_context))
606 @require_http_methods(["GET", "POST"])
607 @signed_terms_required
608 def change_password(request):
609 return password_change(request,
610 post_change_redirect=reverse('edit_profile'),
611 password_change_form=ExtendedPasswordChangeForm)
614 @require_http_methods(["GET", "POST"])
615 @signed_terms_required
617 @transaction.commit_manually
618 def change_email(request, activation_key=None,
619 email_template_name='registration/email_change_email.txt',
620 form_template_name='registration/email_change_form.html',
621 confirm_template_name='registration/email_change_done.html',
625 user = EmailChange.objects.change_email(activation_key)
626 if request.user.is_authenticated() and request.user == user:
627 msg = _('Email changed successfully.')
628 messages.success(request, msg)
630 response = prepare_response(request, user)
633 except ValueError, e:
634 messages.error(request, e)
635 return render_response(confirm_template_name,
636 modified_user=user if 'user' in locals(
638 context_instance=get_context(request,
641 if not request.user.is_authenticated():
642 path = quote(request.get_full_path())
643 url = request.build_absolute_uri(reverse('index'))
644 return HttpResponseRedirect(url + '?next=' + path)
645 form = EmailChangeForm(request.POST or None)
646 if request.method == 'POST' and form.is_valid():
648 ec = form.save(email_template_name, request)
649 except SendMailError, e:
651 messages.error(request, msg)
652 transaction.rollback()
653 except IntegrityError, e:
654 msg = _('There is already a pending change email request.')
655 messages.error(request, msg)
657 msg = _('Change email request has been registered succefully.\
658 You are going to receive a verification email in the new address.')
659 messages.success(request, msg)
661 return render_response(form_template_name,
663 context_instance=get_context(request,
667 @require_http_methods(["GET", "POST"])
668 @signed_terms_required
670 def group_add(request, kind_name='default'):
671 result = callpoint.list_resources()
672 resource_catalog = {'resources':defaultdict(defaultdict),
673 'groups':defaultdict(list)}
674 if result.is_success:
675 for r in result.data:
676 service = r.get('service', '')
677 name = r.get('name', '')
678 group = r.get('group', '')
679 unit = r.get('unit', '')
680 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
681 resource_catalog['resources'][fullname] = dict(unit=unit)
682 resource_catalog['groups'][group].append(fullname)
684 resource_catalog = dict(resource_catalog)
685 for k, v in resource_catalog.iteritems():
686 resource_catalog[k] = dict(v)
690 'Unable to retrieve system resources: %s' % result.reason
694 kind = GroupKind.objects.get(name=kind_name)
696 return HttpResponseBadRequest(_('No such group kind'))
698 resource_presentation = {
700 'help_text':'group compute help text',
703 'help_text':'group storage help text',
705 'pithos+.diskspace': {
706 'help_text':'resource pithos+.diskspace help text',
707 'is_abbreviation':False,
710 'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
711 'is_abbreviation':True,
713 'cyclades.disksize': {
714 'help_text':'resource cyclades.disksize help text',
715 'is_abbreviation':False,
718 'help_text':'resource cyclades.ram help text',
719 'is_abbreviation':True,
722 'help_text':'resource cyclades.cpu help text',
723 'is_abbreviation':True,
727 post_save_redirect = '/im/group/%(id)s/'
728 context_processors = None
729 model, form_class = get_model_and_form_class(
731 form_class=AstakosGroupCreationForm
734 if request.method == 'POST':
735 form = form_class(request.POST, request.FILES)
737 return render_response(
738 template='im/astakosgroup_form_summary.html',
739 context_instance=get_context(request),
740 data=form.cleaned_data,
741 resource_catalog=resource_catalog,
742 resource_presentation=resource_presentation
749 form = form_class(data)
751 # Create the template, context, response
752 template_name = "%s/%s_form_demo.html" % (
753 model._meta.app_label,
754 model._meta.object_name.lower()
756 t = template_loader.get_template(template_name)
757 c = RequestContext(request, {
760 'resource_catalog':resource_catalog,
761 'resource_presentation':resource_presentation,
762 }, context_processors)
763 return HttpResponse(t.render(c))
766 # @require_http_methods(["POST"])
767 # @signed_terms_required
769 def group_add_complete(request):
770 d = dict(request.POST)
771 d['owners'] = [request.user]
772 result = callpoint.create_groups((d,)).next()
773 if result.is_success:
774 new_object = result.data[0]
776 msg = _("The %(verbose_name)s was created successfully.") %\
777 {"verbose_name": model._meta.verbose_name}
778 messages.success(request, msg, fail_silently=True)
780 # # send notification
782 # send_group_creation_notification(
783 # template_name='im/group_creation_notification.txt',
785 # 'group': new_object,
786 # 'owner': request.user,
787 # 'policies': list(form.cleaned_data['policies']),
790 # except SendNotificationError, e:
791 # messages.error(request, e, fail_silently=True)
792 post_save_redirect = '/im/group/%(id)s/'
793 return HttpResponseRedirect(post_save_redirect % new_object)
795 msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
796 {"verbose_name": model._meta.verbose_name,
797 "reason":result.reason}
798 messages.error(request, msg, fail_silently=True)
800 return render_response(
801 template='im/astakosgroup_form_summary.html',
802 context_instance=get_context(request))
805 @require_http_methods(["GET"])
806 @signed_terms_required
808 def group_list(request):
809 none = request.user.astakos_groups.none()
810 q = AstakosGroup.objects.raw("""
811 SELECT auth_group.id,
813 im_groupkind.name AS kindname,
815 owner.email AS groupowner,
816 (SELECT COUNT(*) FROM im_membership
817 WHERE group_id = im_astakosgroup.group_ptr_id
818 AND date_joined IS NOT NULL) AS approved_members_num,
820 SELECT date_joined FROM im_membership
821 WHERE group_id = im_astakosgroup.group_ptr_id
822 AND person_id = %s) IS NULL
823 THEN 0 ELSE 1 END) AS membership_status
825 INNER JOIN im_membership ON (
826 im_astakosgroup.group_ptr_id = im_membership.group_id)
827 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
828 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
829 LEFT JOIN im_astakosuser_owner ON (
830 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
831 LEFT JOIN auth_user as owner ON (
832 im_astakosuser_owner.astakosuser_id = owner.id)
833 WHERE im_membership.person_id = %s
834 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
835 d = defaultdict(list)
837 if request.user.email == g.groupowner:
843 fields = ('own', 'other')
845 v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
847 form = AstakosGroupSortForm({'sort_by': v})
848 if not form.is_valid():
849 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
850 return object_list(request, queryset=none,
851 extra_context={'is_search': False,
854 'own_sorting': own_sorting,
855 'other_sorting': other_sorting,
856 'own_page': request.GET.get('own_page', 1),
857 'other_page': request.GET.get('other_page', 1)
861 @require_http_methods(["GET", "POST"])
862 @signed_terms_required
864 def group_detail(request, group_id):
865 q = AstakosGroup.objects.select_related().filter(pk=group_id)
867 'is_member': """SELECT CASE WHEN EXISTS(
868 SELECT id FROM im_membership
869 WHERE group_id = im_astakosgroup.group_ptr_id
871 THEN 1 ELSE 0 END""" % request.user.id,
872 'is_owner': """SELECT CASE WHEN EXISTS(
873 SELECT id FROM im_astakosuser_owner
874 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
875 AND astakosuser_id = %s)
876 THEN 1 ELSE 0 END""" % request.user.id,
877 'kindname': """SELECT name FROM im_groupkind
878 WHERE id = im_astakosgroup.kind_id"""})
881 context_processors = None
885 except AstakosGroup.DoesNotExist:
886 raise Http404("No %s found matching the query" % (
887 model._meta.verbose_name))
889 update_form = AstakosGroupUpdateForm(instance=obj)
890 addmembers_form = AddGroupMembersForm()
891 if request.method == 'POST':
894 for k, v in request.POST.iteritems():
895 if k in update_form.fields:
897 if k in addmembers_form.fields:
898 addmembers_data[k] = v
899 update_data = update_data or None
900 addmembers_data = addmembers_data or None
901 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
902 addmembers_form = AddGroupMembersForm(addmembers_data)
903 if update_form.is_valid():
905 if addmembers_form.is_valid():
906 map(obj.approve_member, addmembers_form.valid_users)
907 addmembers_form = AddGroupMembersForm()
909 template_name = "%s/%s_detail.html" % (
910 model._meta.app_label, model._meta.object_name.lower())
911 t = template_loader.get_template(template_name)
912 c = RequestContext(request, {
914 }, context_processors)
917 sorting = request.GET.get('sorting')
919 form = MembersSortForm({'sort_by': sorting})
921 sorting = form.cleaned_data.get('sort_by')
923 extra_context = {'update_form': update_form,
924 'addmembers_form': addmembers_form,
925 'page': request.GET.get('page', 1),
927 for key, value in extra_context.items():
932 response = HttpResponse(t.render(c), mimetype=mimetype)
934 request, response, model, getattr(obj, obj._meta.pk.name))
938 @require_http_methods(["GET", "POST"])
939 @signed_terms_required
941 def group_search(request, extra_context=None, **kwargs):
942 q = request.GET.get('q')
943 sorting = request.GET.get('sorting')
944 if request.method == 'GET':
945 form = AstakosGroupSearchForm({'q': q} if q else None)
947 form = AstakosGroupSearchForm(get_query(request))
949 q = form.cleaned_data['q'].strip()
951 queryset = AstakosGroup.objects.select_related()
952 queryset = queryset.filter(name__contains=q)
953 queryset = queryset.filter(approval_date__isnull=False)
954 queryset = queryset.extra(select={
955 'groupname': DB_REPLACE_GROUP_SCHEME,
956 'kindname': "im_groupkind.name",
957 'approved_members_num': """
958 SELECT COUNT(*) FROM im_membership
959 WHERE group_id = im_astakosgroup.group_ptr_id
960 AND date_joined IS NOT NULL""",
961 'membership_approval_date': """
962 SELECT date_joined FROM im_membership
963 WHERE group_id = im_astakosgroup.group_ptr_id
964 AND person_id = %s""" % request.user.id,
966 SELECT CASE WHEN EXISTS(
967 SELECT date_joined FROM im_membership
968 WHERE group_id = im_astakosgroup.group_ptr_id
970 THEN 1 ELSE 0 END""" % request.user.id,
972 SELECT CASE WHEN EXISTS(
973 SELECT id FROM im_astakosuser_owner
974 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
975 AND astakosuser_id = %s)
976 THEN 1 ELSE 0 END""" % request.user.id})
978 # TODO check sorting value
979 queryset = queryset.order_by(sorting)
981 queryset = AstakosGroup.objects.none()
985 paginate_by=PAGINATE_BY,
986 page=request.GET.get('page') or 1,
987 template_name='im/astakosgroup_list.html',
988 extra_context=dict(form=form,
994 @require_http_methods(["GET"])
995 @signed_terms_required
997 def group_all(request, extra_context=None, **kwargs):
998 q = AstakosGroup.objects.select_related()
999 q = q.filter(approval_date__isnull=False)
1000 q = q.extra(select={
1001 'groupname': DB_REPLACE_GROUP_SCHEME,
1002 'kindname': "im_groupkind.name",
1003 'approved_members_num': """
1004 SELECT COUNT(*) FROM im_membership
1005 WHERE group_id = im_astakosgroup.group_ptr_id
1006 AND date_joined IS NOT NULL""",
1007 'membership_approval_date': """
1008 SELECT date_joined FROM im_membership
1009 WHERE group_id = im_astakosgroup.group_ptr_id
1010 AND person_id = %s""" % request.user.id,
1012 SELECT CASE WHEN EXISTS(
1013 SELECT date_joined FROM im_membership
1014 WHERE group_id = im_astakosgroup.group_ptr_id
1016 THEN 1 ELSE 0 END""" % request.user.id})
1017 sorting = request.GET.get('sorting')
1019 # TODO check sorting value
1020 q = q.order_by(sorting)
1024 paginate_by=PAGINATE_BY,
1025 page=request.GET.get('page') or 1,
1026 template_name='im/astakosgroup_list.html',
1027 extra_context=dict(form=AstakosGroupSearchForm(),
1032 @require_http_methods(["POST"])
1033 @signed_terms_required
1035 def group_join(request, group_id):
1036 m = Membership(group_id=group_id,
1037 person=request.user,
1038 date_requested=datetime.now())
1041 post_save_redirect = reverse(
1043 kwargs=dict(group_id=group_id))
1044 return HttpResponseRedirect(post_save_redirect)
1045 except IntegrityError, e:
1047 msg = _('Failed to join group.')
1048 messages.error(request, msg)
1049 return group_search(request)
1052 @require_http_methods(["POST"])
1053 @signed_terms_required
1055 def group_leave(request, group_id):
1057 m = Membership.objects.select_related().get(
1059 person=request.user)
1060 except Membership.DoesNotExist:
1061 return HttpResponseBadRequest(_('Invalid membership.'))
1062 if request.user in m.group.owner.all():
1063 return HttpResponseForbidden(_('Owner can not leave the group.'))
1064 return delete_object(
1068 template_name='im/astakosgroup_list.html',
1069 post_delete_redirect=reverse(
1071 kwargs=dict(group_id=group_id)))
1074 def handle_membership(func):
1076 def wrapper(request, group_id, user_id):
1078 m = Membership.objects.select_related().get(
1081 except Membership.DoesNotExist:
1082 return HttpResponseBadRequest(_('Invalid membership.'))
1084 if request.user not in m.group.owner.all():
1085 return HttpResponseForbidden(_('User is not a group owner.'))
1087 return group_detail(request, group_id)
1091 @require_http_methods(["POST"])
1092 @signed_terms_required
1095 def approve_member(request, membership):
1097 membership.approve()
1098 realname = membership.person.realname
1099 msg = _('%s has been successfully joined the group.' % realname)
1100 messages.success(request, msg)
1101 except BaseException, e:
1103 realname = membership.person.realname
1104 msg = _('Something went wrong during %s\'s approval.' % realname)
1105 messages.error(request, msg)
1108 @signed_terms_required
1111 def disapprove_member(request, membership):
1113 membership.disapprove()
1114 realname = membership.person.realname
1115 msg = _('%s has been successfully removed from the group.' % realname)
1116 messages.success(request, msg)
1117 except BaseException, e:
1119 msg = _('Something went wrong during %s\'s disapproval.' % realname)
1120 messages.error(request, msg)
1123 @require_http_methods(["GET"])
1124 @signed_terms_required
1126 def resource_list(request):
1127 # if request.method == 'POST':
1128 # form = PickResourceForm(request.POST)
1129 # if form.is_valid():
1130 # r = form.cleaned_data.get('resource')
1132 # groups = request.user.membership_set.only('group').filter(
1133 # date_joined__isnull=False)
1134 # groups = [g.group_id for g in groups]
1135 # q = AstakosGroupQuota.objects.select_related().filter(
1136 # resource=r, group__in=groups)
1138 # form = PickResourceForm()
1139 # q = AstakosGroupQuota.objects.none()
1141 # return object_list(request, q,
1142 # template_name='im/astakosuserquota_list.html',
1143 # extra_context={'form': form, 'data':data})
1145 def with_class(entry):
1146 entry['load_class'] = 'red'
1147 max_value = float(entry['maxValue'])
1148 curr_value = float(entry['currValue'])
1149 entry['ratio'] = (curr_value / max_value) * 100
1150 if entry['ratio'] < 66:
1151 entry['load_class'] = 'yellow'
1152 if entry['ratio'] < 33:
1153 entry['load_class'] = 'green'
1156 def pluralize(entry):
1157 entry['plural'] = engine.plural(entry.get('name'))
1160 result = callpoint.get_user_status(request.user.id)
1161 if result.is_success:
1162 backenddata = map(with_class, result.data)
1163 data = map(pluralize, result.data)
1166 messages.error(request, result.reason)
1167 return render_response('im/resource_list.html',
1169 context_instance=get_context(request))
1172 def group_create_list(request):
1173 form = PickResourceForm()
1174 return render_response(
1175 template='im/astakosgroup_create_list.html',
1176 context_instance=get_context(request),)
1179 @require_http_methods(["GET"])
1180 @signed_terms_required
1182 def billing(request):
1184 today = datetime.today()
1185 month_last_day = calendar.monthrange(today.year, today.month)[1]
1186 data['resources'] = map(with_class, data['resources'])
1187 start = request.POST.get('datefrom', None)
1189 today = datetime.fromtimestamp(int(start))
1190 month_last_day = calendar.monthrange(today.year, today.month)[1]
1192 start = datetime(today.year, today.month, 1).strftime("%s")
1193 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1194 r = request_billing.apply(args=('pgerakios@grnet.gr',
1200 status, data = r.result
1201 data = _clear_billing_data(data)
1203 messages.error(request, _('Service response status: %d' % status))
1205 messages.error(request, r.result)
1209 return render_response(
1210 template='im/billing.html',
1211 context_instance=get_context(request),
1213 zerodate=datetime(month=1, year=1970, day=1),
1216 month_last_day=month_last_day)
1219 def _clear_billing_data(data):
1221 # remove addcredits entries
1223 return e['serviceName'] != "addcredits"
1226 def servicefilter(service_name):
1227 service = service_name
1230 return e['serviceName'] == service
1233 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1234 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1235 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1236 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1241 @require_http_methods(["GET"])
1242 @signed_terms_required
1244 def timeline(request):
1245 # data = {'entity':request.user.email}
1247 timeline_header = ()
1248 # form = TimelineForm(data)
1249 form = TimelineForm()
1250 if request.method == 'POST':
1252 form = TimelineForm(data)
1254 data = form.cleaned_data
1255 timeline_header = ('entity', 'resource',
1256 'event name', 'event date',
1257 'incremental cost', 'total cost')
1258 timeline_body = timeline_charge(
1259 data['entity'], data['resource'],
1260 data['start_date'], data['end_date'],
1261 data['details'], data['operation'])
1263 return render_response(template='im/timeline.html',
1264 context_instance=get_context(request),
1266 timeline_header=timeline_header,
1267 timeline_body=timeline_body)