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',
709 'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
711 'cyclades.disksize': {
712 'help_text':'resource cyclades.disksize help text',
715 'help_text':'resource cyclades.ram help text',
719 post_save_redirect = '/im/group/%(id)s/'
720 context_processors = None
721 model, form_class = get_model_and_form_class(
723 form_class=AstakosGroupCreationForm
726 if request.method == 'POST':
727 form = form_class(request.POST, request.FILES)
729 return render_response(
730 template='im/astakosgroup_form_summary.html',
731 context_instance=get_context(request),
732 data=form.cleaned_data
739 form = form_class(data)
741 # Create the template, context, response
742 template_name = "%s/%s_form_demo.html" % (
743 model._meta.app_label,
744 model._meta.object_name.lower()
746 t = template_loader.get_template(template_name)
747 c = RequestContext(request, {
750 'resource_catalog':resource_catalog,
751 'resource_presentation':resource_presentation,
752 }, context_processors)
753 return HttpResponse(t.render(c))
756 # @require_http_methods(["POST"])
757 # @signed_terms_required
759 def group_add_complete(request):
760 d = dict(request.POST)
761 d['owners'] = [request.user]
762 result = callpoint.create_groups((d,)).next()
763 if result.is_success:
764 new_object = result.data[0]
766 msg = _("The %(verbose_name)s was created successfully.") %\
767 {"verbose_name": model._meta.verbose_name}
768 messages.success(request, msg, fail_silently=True)
770 # # send notification
772 # send_group_creation_notification(
773 # template_name='im/group_creation_notification.txt',
775 # 'group': new_object,
776 # 'owner': request.user,
777 # 'policies': list(form.cleaned_data['policies']),
780 # except SendNotificationError, e:
781 # messages.error(request, e, fail_silently=True)
782 post_save_redirect = '/im/group/%(id)s/'
783 return HttpResponseRedirect(post_save_redirect % new_object)
785 msg = _("The %(verbose_name)s creation failed: %(reason)s.") %\
786 {"verbose_name": model._meta.verbose_name,
787 "reason":result.reason}
788 messages.error(request, msg, fail_silently=True)
790 return render_response(
791 template='im/astakosgroup_form_summary.html',
792 context_instance=get_context(request))
795 @require_http_methods(["GET"])
796 @signed_terms_required
798 def group_list(request):
799 none = request.user.astakos_groups.none()
800 q = AstakosGroup.objects.raw("""
801 SELECT auth_group.id,
803 im_groupkind.name AS kindname,
805 owner.email AS groupowner,
806 (SELECT COUNT(*) FROM im_membership
807 WHERE group_id = im_astakosgroup.group_ptr_id
808 AND date_joined IS NOT NULL) AS approved_members_num,
810 SELECT date_joined FROM im_membership
811 WHERE group_id = im_astakosgroup.group_ptr_id
812 AND person_id = %s) IS NULL
813 THEN 0 ELSE 1 END) AS membership_status
815 INNER JOIN im_membership ON (
816 im_astakosgroup.group_ptr_id = im_membership.group_id)
817 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
818 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
819 LEFT JOIN im_astakosuser_owner ON (
820 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
821 LEFT JOIN auth_user as owner ON (
822 im_astakosuser_owner.astakosuser_id = owner.id)
823 WHERE im_membership.person_id = %s
824 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
825 d = defaultdict(list)
827 if request.user.email == g.groupowner:
833 fields = ('own', 'other')
835 v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
837 form = AstakosGroupSortForm({'sort_by': v})
838 if not form.is_valid():
839 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
840 return object_list(request, queryset=none,
841 extra_context={'is_search': False,
844 'own_sorting': own_sorting,
845 'other_sorting': other_sorting,
846 'own_page': request.GET.get('own_page', 1),
847 'other_page': request.GET.get('other_page', 1)
851 @require_http_methods(["GET", "POST"])
852 @signed_terms_required
854 def group_detail(request, group_id):
855 q = AstakosGroup.objects.select_related().filter(pk=group_id)
857 'is_member': """SELECT CASE WHEN EXISTS(
858 SELECT id FROM im_membership
859 WHERE group_id = im_astakosgroup.group_ptr_id
861 THEN 1 ELSE 0 END""" % request.user.id,
862 'is_owner': """SELECT CASE WHEN EXISTS(
863 SELECT id FROM im_astakosuser_owner
864 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
865 AND astakosuser_id = %s)
866 THEN 1 ELSE 0 END""" % request.user.id,
867 'kindname': """SELECT name FROM im_groupkind
868 WHERE id = im_astakosgroup.kind_id"""})
871 context_processors = None
875 except AstakosGroup.DoesNotExist:
876 raise Http404("No %s found matching the query" % (
877 model._meta.verbose_name))
879 update_form = AstakosGroupUpdateForm(instance=obj)
880 addmembers_form = AddGroupMembersForm()
881 if request.method == 'POST':
884 for k, v in request.POST.iteritems():
885 if k in update_form.fields:
887 if k in addmembers_form.fields:
888 addmembers_data[k] = v
889 update_data = update_data or None
890 addmembers_data = addmembers_data or None
891 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
892 addmembers_form = AddGroupMembersForm(addmembers_data)
893 if update_form.is_valid():
895 if addmembers_form.is_valid():
896 map(obj.approve_member, addmembers_form.valid_users)
897 addmembers_form = AddGroupMembersForm()
899 template_name = "%s/%s_detail.html" % (
900 model._meta.app_label, model._meta.object_name.lower())
901 t = template_loader.get_template(template_name)
902 c = RequestContext(request, {
904 }, context_processors)
907 sorting = request.GET.get('sorting')
909 form = MembersSortForm({'sort_by': sorting})
911 sorting = form.cleaned_data.get('sort_by')
913 extra_context = {'update_form': update_form,
914 'addmembers_form': addmembers_form,
915 'page': request.GET.get('page', 1),
917 for key, value in extra_context.items():
922 response = HttpResponse(t.render(c), mimetype=mimetype)
924 request, response, model, getattr(obj, obj._meta.pk.name))
928 @require_http_methods(["GET", "POST"])
929 @signed_terms_required
931 def group_search(request, extra_context=None, **kwargs):
932 q = request.GET.get('q')
933 sorting = request.GET.get('sorting')
934 if request.method == 'GET':
935 form = AstakosGroupSearchForm({'q': q} if q else None)
937 form = AstakosGroupSearchForm(get_query(request))
939 q = form.cleaned_data['q'].strip()
941 queryset = AstakosGroup.objects.select_related()
942 queryset = queryset.filter(name__contains=q)
943 queryset = queryset.filter(approval_date__isnull=False)
944 queryset = queryset.extra(select={
945 'groupname': DB_REPLACE_GROUP_SCHEME,
946 'kindname': "im_groupkind.name",
947 'approved_members_num': """
948 SELECT COUNT(*) FROM im_membership
949 WHERE group_id = im_astakosgroup.group_ptr_id
950 AND date_joined IS NOT NULL""",
951 'membership_approval_date': """
952 SELECT date_joined FROM im_membership
953 WHERE group_id = im_astakosgroup.group_ptr_id
954 AND person_id = %s""" % request.user.id,
956 SELECT CASE WHEN EXISTS(
957 SELECT date_joined FROM im_membership
958 WHERE group_id = im_astakosgroup.group_ptr_id
960 THEN 1 ELSE 0 END""" % request.user.id,
962 SELECT CASE WHEN EXISTS(
963 SELECT id FROM im_astakosuser_owner
964 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
965 AND astakosuser_id = %s)
966 THEN 1 ELSE 0 END""" % request.user.id})
968 # TODO check sorting value
969 queryset = queryset.order_by(sorting)
971 queryset = AstakosGroup.objects.none()
975 paginate_by=PAGINATE_BY,
976 page=request.GET.get('page') or 1,
977 template_name='im/astakosgroup_list.html',
978 extra_context=dict(form=form,
984 @require_http_methods(["GET"])
985 @signed_terms_required
987 def group_all(request, extra_context=None, **kwargs):
988 q = AstakosGroup.objects.select_related()
989 q = q.filter(approval_date__isnull=False)
991 'groupname': DB_REPLACE_GROUP_SCHEME,
992 'kindname': "im_groupkind.name",
993 'approved_members_num': """
994 SELECT COUNT(*) FROM im_membership
995 WHERE group_id = im_astakosgroup.group_ptr_id
996 AND date_joined IS NOT NULL""",
997 'membership_approval_date': """
998 SELECT date_joined FROM im_membership
999 WHERE group_id = im_astakosgroup.group_ptr_id
1000 AND person_id = %s""" % request.user.id,
1002 SELECT CASE WHEN EXISTS(
1003 SELECT date_joined FROM im_membership
1004 WHERE group_id = im_astakosgroup.group_ptr_id
1006 THEN 1 ELSE 0 END""" % request.user.id})
1007 sorting = request.GET.get('sorting')
1009 # TODO check sorting value
1010 q = q.order_by(sorting)
1014 paginate_by=PAGINATE_BY,
1015 page=request.GET.get('page') or 1,
1016 template_name='im/astakosgroup_list.html',
1017 extra_context=dict(form=AstakosGroupSearchForm(),
1022 @require_http_methods(["POST"])
1023 @signed_terms_required
1025 def group_join(request, group_id):
1026 m = Membership(group_id=group_id,
1027 person=request.user,
1028 date_requested=datetime.now())
1031 post_save_redirect = reverse(
1033 kwargs=dict(group_id=group_id))
1034 return HttpResponseRedirect(post_save_redirect)
1035 except IntegrityError, e:
1037 msg = _('Failed to join group.')
1038 messages.error(request, msg)
1039 return group_search(request)
1042 @require_http_methods(["POST"])
1043 @signed_terms_required
1045 def group_leave(request, group_id):
1047 m = Membership.objects.select_related().get(
1049 person=request.user)
1050 except Membership.DoesNotExist:
1051 return HttpResponseBadRequest(_('Invalid membership.'))
1052 if request.user in m.group.owner.all():
1053 return HttpResponseForbidden(_('Owner can not leave the group.'))
1054 return delete_object(
1058 template_name='im/astakosgroup_list.html',
1059 post_delete_redirect=reverse(
1061 kwargs=dict(group_id=group_id)))
1064 def handle_membership(func):
1066 def wrapper(request, group_id, user_id):
1068 m = Membership.objects.select_related().get(
1071 except Membership.DoesNotExist:
1072 return HttpResponseBadRequest(_('Invalid membership.'))
1074 if request.user not in m.group.owner.all():
1075 return HttpResponseForbidden(_('User is not a group owner.'))
1077 return group_detail(request, group_id)
1081 @require_http_methods(["POST"])
1082 @signed_terms_required
1085 def approve_member(request, membership):
1087 membership.approve()
1088 realname = membership.person.realname
1089 msg = _('%s has been successfully joined the group.' % realname)
1090 messages.success(request, msg)
1091 except BaseException, e:
1093 realname = membership.person.realname
1094 msg = _('Something went wrong during %s\'s approval.' % realname)
1095 messages.error(request, msg)
1098 @signed_terms_required
1101 def disapprove_member(request, membership):
1103 membership.disapprove()
1104 realname = membership.person.realname
1105 msg = _('%s has been successfully removed from the group.' % realname)
1106 messages.success(request, msg)
1107 except BaseException, e:
1109 msg = _('Something went wrong during %s\'s disapproval.' % realname)
1110 messages.error(request, msg)
1113 @require_http_methods(["GET"])
1114 @signed_terms_required
1116 def resource_list(request):
1117 # if request.method == 'POST':
1118 # form = PickResourceForm(request.POST)
1119 # if form.is_valid():
1120 # r = form.cleaned_data.get('resource')
1122 # groups = request.user.membership_set.only('group').filter(
1123 # date_joined__isnull=False)
1124 # groups = [g.group_id for g in groups]
1125 # q = AstakosGroupQuota.objects.select_related().filter(
1126 # resource=r, group__in=groups)
1128 # form = PickResourceForm()
1129 # q = AstakosGroupQuota.objects.none()
1131 # return object_list(request, q,
1132 # template_name='im/astakosuserquota_list.html',
1133 # extra_context={'form': form, 'data':data})
1135 def with_class(entry):
1136 entry['load_class'] = 'red'
1137 max_value = float(entry['maxValue'])
1138 curr_value = float(entry['currValue'])
1139 entry['ratio'] = (curr_value / max_value) * 100
1140 if entry['ratio'] < 66:
1141 entry['load_class'] = 'yellow'
1142 if entry['ratio'] < 33:
1143 entry['load_class'] = 'green'
1146 def pluralize(entry):
1147 entry['plural'] = engine.plural(entry.get('name'))
1150 result = callpoint.get_user_status(request.user.id)
1151 if result.is_success:
1152 backenddata = map(with_class, result.data)
1153 data = map(pluralize, result.data)
1156 messages.error(request, result.reason)
1157 return render_response('im/resource_list.html',
1159 context_instance=get_context(request))
1162 def group_create_list(request):
1163 form = PickResourceForm()
1164 return render_response(
1165 template='im/astakosgroup_create_list.html',
1166 context_instance=get_context(request),)
1169 @require_http_methods(["GET"])
1170 @signed_terms_required
1172 def billing(request):
1174 today = datetime.today()
1175 month_last_day = calendar.monthrange(today.year, today.month)[1]
1176 data['resources'] = map(with_class, data['resources'])
1177 start = request.POST.get('datefrom', None)
1179 today = datetime.fromtimestamp(int(start))
1180 month_last_day = calendar.monthrange(today.year, today.month)[1]
1182 start = datetime(today.year, today.month, 1).strftime("%s")
1183 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1184 r = request_billing.apply(args=('pgerakios@grnet.gr',
1190 status, data = r.result
1191 data = _clear_billing_data(data)
1193 messages.error(request, _('Service response status: %d' % status))
1195 messages.error(request, r.result)
1199 return render_response(
1200 template='im/billing.html',
1201 context_instance=get_context(request),
1203 zerodate=datetime(month=1, year=1970, day=1),
1206 month_last_day=month_last_day)
1209 def _clear_billing_data(data):
1211 # remove addcredits entries
1213 return e['serviceName'] != "addcredits"
1216 def servicefilter(service_name):
1217 service = service_name
1220 return e['serviceName'] == service
1223 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1224 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1225 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1226 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1231 @require_http_methods(["GET"])
1232 @signed_terms_required
1234 def timeline(request):
1235 # data = {'entity':request.user.email}
1237 timeline_header = ()
1238 # form = TimelineForm(data)
1239 form = TimelineForm()
1240 if request.method == 'POST':
1242 form = TimelineForm(data)
1244 data = form.cleaned_data
1245 timeline_header = ('entity', 'resource',
1246 'event name', 'event date',
1247 'incremental cost', 'total cost')
1248 timeline_body = timeline_charge(
1249 data['entity'], data['resource'],
1250 data['start_date'], data['end_date'],
1251 data['details'], data['operation'])
1253 return render_response(template='im/timeline.html',
1254 context_instance=get_context(request),
1256 timeline_header=timeline_header,
1257 timeline_body=timeline_body)