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 AstakosDjangoDBCallpoint
92 logger = logging.getLogger(__name__)
95 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
98 callpoint = AstakosDjangoDBCallpoint()
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 @signed_terms_required
669 def group_add(request, kind_name='default'):
671 kind = GroupKind.objects.get(name=kind_name)
673 return HttpResponseBadRequest(_('No such group kind'))
675 post_save_redirect = '/im/group/%(id)s/'
676 context_processors = None
677 model, form_class = get_model_and_form_class(
679 form_class=AstakosGroupCreationForm
682 (str(r.id), r) for r in Resource.objects.select_related().all())
684 if request.method == 'POST':
685 form = form_class(request.POST, request.FILES, resources=resources)
687 new_object = form.save()
690 new_object.owners = [request.user]
692 # save quota policies
693 for (rid, uplimit) in form.resources():
698 # TODO Should I stay or should I go???
701 new_object.astakosgroupquota_set.create(
705 policies.append('%s %d' % (r, uplimit))
706 msg = _("The %(verbose_name)s was created successfully.") %\
707 {"verbose_name": model._meta.verbose_name}
708 messages.success(request, msg, fail_silently=True)
712 send_group_creation_notification(
713 template_name='im/group_creation_notification.txt',
716 'owner': request.user,
717 'policies': policies,
720 except SendNotificationError, e:
721 messages.error(request, e, fail_silently=True)
722 return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
728 form = form_class(data, resources=resources)
730 # Create the template, context, response
731 template_name = "%s/%s_form.html" % (
732 model._meta.app_label,
733 model._meta.object_name.lower()
735 t = template_loader.get_template(template_name)
736 c = RequestContext(request, {
739 }, context_processors)
740 return HttpResponse(t.render(c))
743 @signed_terms_required
745 def group_list(request):
746 none = request.user.astakos_groups.none()
747 q = AstakosGroup.objects.raw("""
748 SELECT auth_group.id,
750 im_groupkind.name AS kindname,
752 owner.email AS groupowner,
753 (SELECT COUNT(*) FROM im_membership
754 WHERE group_id = im_astakosgroup.group_ptr_id
755 AND date_joined IS NOT NULL) AS approved_members_num,
757 SELECT date_joined FROM im_membership
758 WHERE group_id = im_astakosgroup.group_ptr_id
759 AND person_id = %s) IS NULL
760 THEN 0 ELSE 1 END) AS membership_status
762 INNER JOIN im_membership ON (
763 im_astakosgroup.group_ptr_id = im_membership.group_id)
764 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
765 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
766 LEFT JOIN im_astakosuser_owner ON (
767 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
768 LEFT JOIN auth_user as owner ON (
769 im_astakosuser_owner.astakosuser_id = owner.id)
770 WHERE im_membership.person_id = %s
771 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
772 d = defaultdict(list)
774 if request.user.email == g.groupowner:
780 fields = ('own', 'other')
782 v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
784 form = AstakosGroupSortForm({'sort_by': v})
785 if not form.is_valid():
786 globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
787 return object_list(request, queryset=none,
788 extra_context={'is_search': False,
791 'own_sorting': own_sorting,
792 'other_sorting': other_sorting,
793 'own_page': request.GET.get('own_page', 1),
794 'other_page': request.GET.get('other_page', 1)
798 @signed_terms_required
800 def group_detail(request, group_id):
801 q = AstakosGroup.objects.select_related().filter(pk=group_id)
803 'is_member': """SELECT CASE WHEN EXISTS(
804 SELECT id FROM im_membership
805 WHERE group_id = im_astakosgroup.group_ptr_id
807 THEN 1 ELSE 0 END""" % request.user.id,
808 'is_owner': """SELECT CASE WHEN EXISTS(
809 SELECT id FROM im_astakosuser_owner
810 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
811 AND astakosuser_id = %s)
812 THEN 1 ELSE 0 END""" % request.user.id,
813 'kindname': """SELECT name FROM im_groupkind
814 WHERE id = im_astakosgroup.kind_id"""})
817 context_processors = None
821 except AstakosGroup.DoesNotExist:
822 raise Http404("No %s found matching the query" % (
823 model._meta.verbose_name))
825 update_form = AstakosGroupUpdateForm(instance=obj)
826 addmembers_form = AddGroupMembersForm()
827 if request.method == 'POST':
830 for k, v in request.POST.iteritems():
831 if k in update_form.fields:
833 if k in addmembers_form.fields:
834 addmembers_data[k] = v
835 update_data = update_data or None
836 addmembers_data = addmembers_data or None
837 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
838 addmembers_form = AddGroupMembersForm(addmembers_data)
839 if update_form.is_valid():
841 if addmembers_form.is_valid():
842 map(obj.approve_member, addmembers_form.valid_users)
843 addmembers_form = AddGroupMembersForm()
845 template_name = "%s/%s_detail.html" % (
846 model._meta.app_label, model._meta.object_name.lower())
847 t = template_loader.get_template(template_name)
848 c = RequestContext(request, {
850 }, context_processors)
853 sorting = request.GET.get('sorting')
855 form = MembersSortForm({'sort_by': sorting})
857 sorting = form.cleaned_data.get('sort_by')
859 extra_context = {'update_form': update_form,
860 'addmembers_form': addmembers_form,
861 'page': request.GET.get('page', 1),
863 for key, value in extra_context.items():
868 response = HttpResponse(t.render(c), mimetype=mimetype)
870 request, response, model, getattr(obj, obj._meta.pk.name))
874 @signed_terms_required
876 def group_search(request, extra_context=None, **kwargs):
877 q = request.GET.get('q')
878 sorting = request.GET.get('sorting')
879 if request.method == 'GET':
880 form = AstakosGroupSearchForm({'q': q} if q else None)
882 form = AstakosGroupSearchForm(get_query(request))
884 q = form.cleaned_data['q'].strip()
886 queryset = AstakosGroup.objects.select_related()
887 queryset = queryset.filter(name__contains=q)
888 queryset = queryset.filter(approval_date__isnull=False)
889 queryset = queryset.extra(select={
890 'groupname': DB_REPLACE_GROUP_SCHEME,
891 'kindname': "im_groupkind.name",
892 'approved_members_num': """
893 SELECT COUNT(*) FROM im_membership
894 WHERE group_id = im_astakosgroup.group_ptr_id
895 AND date_joined IS NOT NULL""",
896 'membership_approval_date': """
897 SELECT date_joined FROM im_membership
898 WHERE group_id = im_astakosgroup.group_ptr_id
899 AND person_id = %s""" % request.user.id,
901 SELECT CASE WHEN EXISTS(
902 SELECT date_joined FROM im_membership
903 WHERE group_id = im_astakosgroup.group_ptr_id
905 THEN 1 ELSE 0 END""" % request.user.id,
907 SELECT CASE WHEN EXISTS(
908 SELECT id FROM im_astakosuser_owner
909 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
910 AND astakosuser_id = %s)
911 THEN 1 ELSE 0 END""" % request.user.id})
913 # TODO check sorting value
914 queryset = queryset.order_by(sorting)
916 queryset = AstakosGroup.objects.none()
920 paginate_by=PAGINATE_BY,
921 page=request.GET.get('page') or 1,
922 template_name='im/astakosgroup_list.html',
923 extra_context=dict(form=form,
929 @signed_terms_required
931 def group_all(request, extra_context=None, **kwargs):
932 q = AstakosGroup.objects.select_related()
933 q = q.filter(approval_date__isnull=False)
935 'groupname': DB_REPLACE_GROUP_SCHEME,
936 'kindname': "im_groupkind.name",
937 'approved_members_num': """
938 SELECT COUNT(*) FROM im_membership
939 WHERE group_id = im_astakosgroup.group_ptr_id
940 AND date_joined IS NOT NULL""",
941 'membership_approval_date': """
942 SELECT date_joined FROM im_membership
943 WHERE group_id = im_astakosgroup.group_ptr_id
944 AND person_id = %s""" % request.user.id,
946 SELECT CASE WHEN EXISTS(
947 SELECT date_joined FROM im_membership
948 WHERE group_id = im_astakosgroup.group_ptr_id
950 THEN 1 ELSE 0 END""" % request.user.id})
951 sorting = request.GET.get('sorting')
953 # TODO check sorting value
954 q = q.order_by(sorting)
958 paginate_by=PAGINATE_BY,
959 page=request.GET.get('page') or 1,
960 template_name='im/astakosgroup_list.html',
961 extra_context=dict(form=AstakosGroupSearchForm(),
966 @signed_terms_required
968 def group_join(request, group_id):
969 m = Membership(group_id=group_id,
971 date_requested=datetime.now())
974 post_save_redirect = reverse(
976 kwargs=dict(group_id=group_id))
977 return HttpResponseRedirect(post_save_redirect)
978 except IntegrityError, e:
980 msg = _('Failed to join group.')
981 messages.error(request, msg)
982 return group_search(request)
985 @signed_terms_required
987 def group_leave(request, group_id):
989 m = Membership.objects.select_related().get(
992 except Membership.DoesNotExist:
993 return HttpResponseBadRequest(_('Invalid membership.'))
994 if request.user in m.group.owner.all():
995 return HttpResponseForbidden(_('Owner can not leave the group.'))
996 return delete_object(
1000 template_name='im/astakosgroup_list.html',
1001 post_delete_redirect=reverse(
1003 kwargs=dict(group_id=group_id)))
1006 def handle_membership(func):
1008 def wrapper(request, group_id, user_id):
1010 m = Membership.objects.select_related().get(
1013 except Membership.DoesNotExist:
1014 return HttpResponseBadRequest(_('Invalid membership.'))
1016 if request.user not in m.group.owner.all():
1017 return HttpResponseForbidden(_('User is not a group owner.'))
1019 return group_detail(request, group_id)
1023 @signed_terms_required
1026 def approve_member(request, membership):
1028 membership.approve()
1029 realname = membership.person.realname
1030 msg = _('%s has been successfully joined the group.' % realname)
1031 messages.success(request, msg)
1032 except BaseException, e:
1034 realname = membership.person.realname
1035 msg = _('Something went wrong during %s\'s approval.' % realname)
1036 messages.error(request, msg)
1039 @signed_terms_required
1042 def disapprove_member(request, membership):
1044 membership.disapprove()
1045 realname = membership.person.realname
1046 msg = _('%s has been successfully removed from the group.' % realname)
1047 messages.success(request, msg)
1048 except BaseException, e:
1050 msg = _('Something went wrong during %s\'s disapproval.' % realname)
1051 messages.error(request, msg)
1054 @signed_terms_required
1056 def resource_list(request):
1057 # if request.method == 'POST':
1058 # form = PickResourceForm(request.POST)
1059 # if form.is_valid():
1060 # r = form.cleaned_data.get('resource')
1062 # groups = request.user.membership_set.only('group').filter(
1063 # date_joined__isnull=False)
1064 # groups = [g.group_id for g in groups]
1065 # q = AstakosGroupQuota.objects.select_related().filter(
1066 # resource=r, group__in=groups)
1068 # form = PickResourceForm()
1069 # q = AstakosGroupQuota.objects.none()
1071 # return object_list(request, q,
1072 # template_name='im/astakosuserquota_list.html',
1073 # extra_context={'form': form, 'data':data})
1075 def with_class(entry):
1076 entry['load_class'] = 'red'
1077 max_value = float(entry['maxValue'])
1078 curr_value = float(entry['currValue'])
1079 entry['ratio'] = (curr_value / max_value) * 100
1080 if entry['ratio'] < 66:
1081 entry['load_class'] = 'yellow'
1082 if entry['ratio'] < 33:
1083 entry['load_class'] = 'green'
1086 def pluralize(entry):
1087 entry['plural'] = engine.plural(entry.get('name'))
1091 data = callpoint.get_user_status(request.user.id)
1092 except Exception, e:
1094 messages.error(request, e)
1096 backenddata = map(with_class, data)
1097 data = map(pluralize, data)
1098 return render_response('im/resource_list.html',
1100 context_instance=get_context(request))
1102 @signed_terms_required
1104 def group_create_demo(request, kind_name='default'):
1105 resources = callpoint.list_resources()
1106 resource_catalog = {'resources':defaultdict(defaultdict),
1107 'groups':defaultdict(list)}
1109 service = r.get('service', '')
1110 name = r.get('name', '')
1111 group = r.get('group', '')
1112 unit = r.get('unit', '')
1113 fullname = '%s%s%s' % (service, RESOURCE_SEPARATOR, name)
1114 resource_catalog['resources'][fullname] = dict(unit=unit)
1115 resource_catalog['groups'][group].append(fullname)
1117 resource_catalog = dict(resource_catalog)
1118 for k, v in resource_catalog.iteritems():
1119 resource_catalog[k] = dict(v)
1121 kind = GroupKind.objects.get(name=kind_name)
1123 return HttpResponseBadRequest(_('No such group kind'))
1125 post_save_redirect = '/im/group/%(id)s/'
1126 context_processors = None
1127 model, form_class = get_model_and_form_class(
1129 form_class=AstakosGroupCreationForm
1132 if request.method == 'POST':
1133 form = form_class(request.POST, request.FILES)
1135 new_object = form.save()
1136 new_object.policies = form.policies()
1139 new_object.owners = [request.user]
1141 msg = _("The %(verbose_name)s was created successfully.") %\
1142 {"verbose_name": model._meta.verbose_name}
1143 messages.success(request, msg, fail_silently=True)
1147 send_group_creation_notification(
1148 template_name='im/group_creation_notification.txt',
1150 'group': new_object,
1151 'owner': request.user,
1152 'policies': list(form.policies()),
1155 except SendNotificationError, e:
1156 messages.error(request, e, fail_silently=True)
1157 return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
1159 now = datetime.now()
1163 form = form_class(data)
1165 # Create the template, context, response
1166 template_name = "%s/%s_form_demo.html" % (
1167 model._meta.app_label,
1168 model._meta.object_name.lower()
1170 t = template_loader.get_template(template_name)
1171 c = RequestContext(request, {
1174 'resource_catalog':resource_catalog
1175 }, context_processors)
1176 return HttpResponse(t.render(c))
1179 def group_create_list(request):
1180 form = PickResourceForm()
1181 return render_response(
1182 template='im/astakosgroup_create_list.html',
1183 context_instance=get_context(request),)
1186 @signed_terms_required
1188 def billing(request):
1190 today = datetime.today()
1191 month_last_day = calendar.monthrange(today.year, today.month)[1]
1192 data['resources'] = map(with_class, data['resources'])
1193 start = request.POST.get('datefrom', None)
1195 today = datetime.fromtimestamp(int(start))
1196 month_last_day = calendar.monthrange(today.year, today.month)[1]
1198 start = datetime(today.year, today.month, 1).strftime("%s")
1199 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1200 r = request_billing.apply(args=('pgerakios@grnet.gr',
1206 status, data = r.result
1207 data = _clear_billing_data(data)
1209 messages.error(request, _('Service response status: %d' % status))
1211 messages.error(request, r.result)
1215 return render_response(
1216 template='im/billing.html',
1217 context_instance=get_context(request),
1219 zerodate=datetime(month=1, year=1970, day=1),
1222 month_last_day=month_last_day)
1225 def _clear_billing_data(data):
1227 # remove addcredits entries
1229 return e['serviceName'] != "addcredits"
1232 def servicefilter(service_name):
1233 service = service_name
1236 return e['serviceName'] == service
1239 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1240 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1241 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1242 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1247 @signed_terms_required
1249 def timeline(request):
1250 # data = {'entity':request.user.email}
1252 timeline_header = ()
1253 # form = TimelineForm(data)
1254 form = TimelineForm()
1255 if request.method == 'POST':
1257 form = TimelineForm(data)
1259 data = form.cleaned_data
1260 timeline_header = ('entity', 'resource',
1261 'event name', 'event date',
1262 'incremental cost', 'total cost')
1263 timeline_body = timeline_charge(
1264 data['entity'], data['resource'],
1265 data['start_date'], data['end_date'],
1266 data['details'], data['operation'])
1268 return render_response(template='im/timeline.html',
1269 context_instance=get_context(request),
1271 timeline_header=timeline_header,
1272 timeline_body=timeline_body)