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
70 from django.db.models.query import QuerySet
72 from astakos.im.activation_backends import get_backend, SimpleBackend
73 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
74 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
75 FeedbackForm, SignApprovalTermsForm,
76 ExtendedPasswordChangeForm, EmailChangeForm,
77 AstakosGroupCreationForm, AstakosGroupSearchForm,
78 AstakosGroupUpdateForm, AddGroupMembersForm,
79 AstakosGroupSortForm, MembersSortForm,
80 TimelineForm, PickResourceForm,
81 AstakosGroupCreationSummaryForm)
82 from astakos.im.functions import (send_feedback, SendMailError,
83 logout as auth_logout,
84 activate as activate_func,
85 switch_account_to_shibboleth,
86 send_group_creation_notification,
87 SendNotificationError)
88 from astakos.im.endpoints.qh import timeline_charge
89 from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
90 LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA)
91 from astakos.im.tasks import request_billing
92 from astakos.im.api.callpoint import AstakosCallpoint
94 import astakos.im.messages as astakos_messages
96 logger = logging.getLogger(__name__)
99 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
102 callpoint = AstakosCallpoint()
104 def render_response(template, tab=None, status=200, reset_cookie=False,
105 context_instance=None, **kwargs):
107 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
108 keyword argument and returns an ``django.http.HttpResponse`` with the
109 specified ``status``.
112 tab = template.partition('_')[0].partition('.html')[0]
113 kwargs.setdefault('tab', tab)
114 html = template_loader.render_to_string(
115 template, kwargs, context_instance=context_instance)
116 response = HttpResponse(html, status=status)
118 set_cookie(response, context_instance['request'].user)
122 def requires_anonymous(func):
124 Decorator checkes whether the request.user is not Anonymous and in that case
125 redirects to `logout`.
128 def wrapper(request, *args):
129 if not request.user.is_anonymous():
130 next = urlencode({'next': request.build_absolute_uri()})
131 logout_uri = reverse(logout) + '?' + next
132 return HttpResponseRedirect(logout_uri)
133 return func(request, *args)
137 def signed_terms_required(func):
139 Decorator checkes whether the request.user is Anonymous and in that case
140 redirects to `logout`.
143 def wrapper(request, *args, **kwargs):
144 if request.user.is_authenticated() and not request.user.signed_terms:
145 params = urlencode({'next': request.build_absolute_uri(),
147 terms_uri = reverse('latest_terms') + '?' + params
148 return HttpResponseRedirect(terms_uri)
149 return func(request, *args, **kwargs)
153 @require_http_methods(["GET", "POST"])
154 @signed_terms_required
155 def index(request, login_template_name='im/login.html', extra_context=None):
157 If there is logged on user renders the profile page otherwise renders login page.
161 ``login_template_name``
162 A custom login template to use. This is optional; if not specified,
163 this will default to ``im/login.html``.
165 ``profile_template_name``
166 A custom profile template to use. This is optional; if not specified,
167 this will default to ``im/profile.html``.
170 An dictionary of variables to add to the template context.
174 im/profile.html or im/login.html or ``template_name`` keyword argument.
177 template_name = login_template_name
178 if request.user.is_authenticated():
179 return HttpResponseRedirect(reverse('edit_profile'))
180 return render_response(template_name,
181 login_form=LoginForm(request=request),
182 context_instance=get_context(request, extra_context))
185 @require_http_methods(["GET", "POST"])
187 @signed_terms_required
188 @transaction.commit_manually
189 def invite(request, template_name='im/invitations.html', extra_context=None):
191 Allows a user to invite somebody else.
193 In case of GET request renders a form for providing the invitee information.
194 In case of POST checks whether the user has not run out of invitations and then
195 sends an invitation email to singup to the service.
197 The view uses commit_manually decorator in order to ensure the number of the
198 user invitations is going to be updated only if the email has been successfully sent.
200 If the user isn't logged in, redirects to settings.LOGIN_URL.
205 A custom template to use. This is optional; if not specified,
206 this will default to ``im/invitations.html``.
209 An dictionary of variables to add to the template context.
213 im/invitations.html or ``template_name`` keyword argument.
217 The view expectes the following settings are defined:
219 * LOGIN_URL: login uri
220 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
224 form = InvitationForm()
226 inviter = request.user
227 if request.method == 'POST':
228 form = InvitationForm(request.POST)
229 if inviter.invitations > 0:
232 email = form.cleaned_data.get('username')
233 realname = form.cleaned_data.get('realname')
234 inviter.invite(email, realname)
235 message = _(astakos_messages.INVITATION_SENT) % locals()
236 messages.success(request, message)
237 except SendMailError, e:
239 messages.error(request, message)
240 transaction.rollback()
241 except BaseException, e:
242 message = _(astakos_messages.GENERIC_ERROR)
243 messages.error(request, message)
245 transaction.rollback()
249 message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
250 messages.error(request, message)
252 sent = [{'email': inv.username,
253 'realname': inv.realname,
254 'is_consumed': inv.is_consumed}
255 for inv in request.user.invitations_sent.all()]
256 kwargs = {'inviter': inviter,
258 context = get_context(request, extra_context, **kwargs)
259 return render_response(template_name,
260 invitation_form=form,
261 context_instance=context)
264 @require_http_methods(["GET", "POST"])
266 @signed_terms_required
267 def edit_profile(request, template_name='im/profile.html', extra_context=None):
269 Allows a user to edit his/her profile.
271 In case of GET request renders a form for displaying the user information.
272 In case of POST updates the user informantion and redirects to ``next``
273 url parameter if exists.
275 If the user isn't logged in, redirects to settings.LOGIN_URL.
280 A custom template to use. This is optional; if not specified,
281 this will default to ``im/profile.html``.
284 An dictionary of variables to add to the template context.
288 im/profile.html or ``template_name`` keyword argument.
292 The view expectes the following settings are defined:
294 * LOGIN_URL: login uri
296 extra_context = extra_context or {}
297 form = ProfileForm(instance=request.user)
298 extra_context['next'] = request.GET.get('next')
300 if request.method == 'POST':
301 form = ProfileForm(request.POST, instance=request.user)
304 prev_token = request.user.auth_token
306 reset_cookie = user.auth_token != prev_token
307 form = ProfileForm(instance=user)
308 next = request.POST.get('next')
310 return redirect(next)
311 msg = _(astakos_messages.PROFILE_UPDATED)
312 messages.success(request, msg)
313 except ValueError, ve:
314 messages.success(request, ve)
315 elif request.method == "GET":
316 if not request.user.is_verified:
317 request.user.is_verified = True
319 return render_response(template_name,
320 reset_cookie=reset_cookie,
322 context_instance=get_context(request,
326 @transaction.commit_manually
327 @require_http_methods(["GET", "POST"])
328 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
330 Allows a user to create a local account.
332 In case of GET request renders a form for entering the user information.
333 In case of POST handles the signup.
335 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
336 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
337 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
338 (see activation_backends);
340 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
341 otherwise renders the same page with a success message.
343 On unsuccessful creation, renders ``template_name`` with an error message.
348 A custom template to render. This is optional;
349 if not specified, this will default to ``im/signup.html``.
352 A custom template to render in case of success. This is optional;
353 if not specified, this will default to ``im/signup_complete.html``.
356 An dictionary of variables to add to the template context.
360 im/signup.html or ``template_name`` keyword argument.
361 im/signup_complete.html or ``on_success`` keyword argument.
363 if request.user.is_authenticated():
364 return HttpResponseRedirect(reverse('edit_profile'))
366 provider = get_query(request).get('provider', 'local')
369 backend = get_backend(request)
370 form = backend.get_signup_form(provider)
372 form = SimpleBackend(request).get_signup_form(provider)
373 messages.error(request, e)
374 if request.method == 'POST':
376 user = form.save(commit=False)
378 result = backend.handle_activation(user)
379 status = messages.SUCCESS
380 message = result.message
382 if 'additional_email' in form.cleaned_data:
383 additional_email = form.cleaned_data['additional_email']
384 if additional_email != user.email:
385 user.additionalmail_set.create(email=additional_email)
386 msg = 'Additional email: %s saved for user %s.' % (
387 additional_email, user.email)
388 logger.log(LOGGING_LEVEL, msg)
389 if user and user.is_active:
390 next = request.POST.get('next', '')
391 response = prepare_response(request, user, next=next)
394 messages.add_message(request, status, message)
396 return render_response(on_success,
397 context_instance=get_context(request, extra_context))
398 except SendMailError, e:
400 messages.error(request, message)
401 transaction.rollback()
402 except BaseException, e:
403 message = _(astakos_messages.GENERIC_ERROR)
404 messages.error(request, message)
406 transaction.rollback()
407 return render_response(template_name,
410 context_instance=get_context(request, extra_context))
413 @require_http_methods(["GET", "POST"])
415 @signed_terms_required
416 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
418 Allows a user to send feedback.
420 In case of GET request renders a form for providing the feedback information.
421 In case of POST sends an email to support team.
423 If the user isn't logged in, redirects to settings.LOGIN_URL.
428 A custom template to use. This is optional; if not specified,
429 this will default to ``im/feedback.html``.
432 An dictionary of variables to add to the template context.
436 im/signup.html or ``template_name`` keyword argument.
440 * LOGIN_URL: login uri
441 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
443 if request.method == 'GET':
444 form = FeedbackForm()
445 if request.method == 'POST':
447 return HttpResponse('Unauthorized', status=401)
449 form = FeedbackForm(request.POST)
451 msg = form.cleaned_data['feedback_msg']
452 data = form.cleaned_data['feedback_data']
454 send_feedback(msg, data, request.user, email_template_name)
455 except SendMailError, e:
456 messages.error(request, message)
458 message = _(astakos_messages.FEEDBACK_SENT)
459 messages.success(request, message)
460 return render_response(template_name,
462 context_instance=get_context(request, extra_context))
465 @require_http_methods(["GET", "POST"])
466 @signed_terms_required
467 def logout(request, template='registration/logged_out.html', extra_context=None):
469 Wraps `django.contrib.auth.logout` and delete the cookie.
471 response = HttpResponse()
472 if request.user.is_authenticated():
473 email = request.user.email
475 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
476 msg = 'Cookie deleted for %s' % email
477 logger.log(LOGGING_LEVEL, msg)
478 next = request.GET.get('next')
480 response['Location'] = next
481 response.status_code = 302
484 response['Location'] = LOGOUT_NEXT
485 response.status_code = 301
487 messages.success(request, _(astakos_messages.LOGOUT_SUCCESS))
488 context = get_context(request, extra_context)
490 template_loader.render_to_string(template, context_instance=context))
494 @require_http_methods(["GET", "POST"])
495 @transaction.commit_manually
496 def activate(request, greeting_email_template_name='im/welcome_email.txt',
497 helpdesk_email_template_name='im/helpdesk_notification.txt'):
499 Activates the user identified by the ``auth`` request parameter, sends a welcome email
500 and renews the user token.
502 The view uses commit_manually decorator in order to ensure the user state will be updated
503 only if the email will be send successfully.
505 token = request.GET.get('auth')
506 next = request.GET.get('next')
508 user = AstakosUser.objects.get(auth_token=token)
509 except AstakosUser.DoesNotExist:
510 return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
513 message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
514 messages.error(request, message)
515 return index(request)
518 local_user = AstakosUser.objects.get(
523 except AstakosUser.DoesNotExist:
527 greeting_email_template_name,
528 helpdesk_email_template_name,
531 response = prepare_response(request, user, next, renew=True)
534 except SendMailError, e:
536 messages.error(request, message)
537 transaction.rollback()
538 return index(request)
539 except BaseException, e:
540 message = _(astakos_messages.GENERIC_ERROR)
541 messages.error(request, message)
543 transaction.rollback()
544 return index(request)
547 user = switch_account_to_shibboleth(
550 greeting_email_template_name
552 response = prepare_response(request, user, next, renew=True)
555 except SendMailError, e:
557 messages.error(request, message)
558 transaction.rollback()
559 return index(request)
560 except BaseException, e:
561 message = _(astakos_messages.GENERIC_ERROR)
562 messages.error(request, message)
564 transaction.rollback()
565 return index(request)
568 @require_http_methods(["GET", "POST"])
569 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
574 term = ApprovalTerms.objects.order_by('-id')[0]
579 term = ApprovalTerms.objects.get(id=term_id)
580 except ApprovalTerms.DoesNotExist, e:
584 messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
585 return HttpResponseRedirect(reverse('index'))
586 f = open(term.location, 'r')
589 if request.method == 'POST':
590 next = request.POST.get('next')
592 next = reverse('index')
593 form = SignApprovalTermsForm(request.POST, instance=request.user)
594 if not form.is_valid():
595 return render_response(template_name,
597 approval_terms_form=form,
598 context_instance=get_context(request, extra_context))
600 return HttpResponseRedirect(next)
603 if request.user.is_authenticated() and not request.user.signed_terms:
604 form = SignApprovalTermsForm(instance=request.user)
605 return render_response(template_name,
607 approval_terms_form=form,
608 context_instance=get_context(request, extra_context))
611 @require_http_methods(["GET", "POST"])
612 @signed_terms_required
613 def change_password(request):
614 return password_change(request,
615 post_change_redirect=reverse('edit_profile'),
616 password_change_form=ExtendedPasswordChangeForm)
619 @require_http_methods(["GET", "POST"])
620 @signed_terms_required
622 @transaction.commit_manually
623 def change_email(request, activation_key=None,
624 email_template_name='registration/email_change_email.txt',
625 form_template_name='registration/email_change_form.html',
626 confirm_template_name='registration/email_change_done.html',
630 user = EmailChange.objects.change_email(activation_key)
631 if request.user.is_authenticated() and request.user == user:
632 msg = _(astakos_messages.EMAIL_CHANGED)
633 messages.success(request, msg)
635 response = prepare_response(request, user)
638 except ValueError, e:
639 messages.error(request, e)
640 return render_response(confirm_template_name,
641 modified_user=user if 'user' in locals(
643 context_instance=get_context(request,
646 if not request.user.is_authenticated():
647 path = quote(request.get_full_path())
648 url = request.build_absolute_uri(reverse('index'))
649 return HttpResponseRedirect(url + '?next=' + path)
650 form = EmailChangeForm(request.POST or None)
651 if request.method == 'POST' and form.is_valid():
653 ec = form.save(email_template_name, request)
654 except SendMailError, e:
656 messages.error(request, msg)
657 transaction.rollback()
658 except IntegrityError, e:
659 msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
660 messages.error(request, msg)
662 msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
663 messages.success(request, msg)
665 return render_response(form_template_name,
667 context_instance=get_context(request,
670 class ResourcePresentation():
672 def __init__(self, data):
675 def update_from_result(self, result):
676 if result.is_success:
677 for r in result.data:
678 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
679 if not rname in self.data['resources']:
680 self.data['resources'][rname] = {}
682 self.data['resources'][rname].update(r)
683 self.data['resources'][rname]['id'] = rname
684 group = r.get('group')
685 if not group in self.data['groups']:
686 self.data['groups'][group] = {}
688 self.data['groups'][r.get('group')].update({'name': r.get('group')})
690 def test(self, quota_dict):
691 for k, v in quota_dict.iteritems():
694 if not rname in self.data['resources']:
695 self.data['resources'][rname] = {}
698 self.data['resources'][rname]['value'] = value
701 def update_from_result_report(self, result):
702 if result.is_success:
703 for r in result.data:
704 rname = r.get('name')
705 if not rname in self.data['resources']:
706 self.data['resources'][rname] = {}
708 self.data['resources'][rname].update(r)
709 self.data['resources'][rname]['id'] = rname
710 group = r.get('group')
711 if not group in self.data['groups']:
712 self.data['groups'][group] = {}
714 self.data['groups'][r.get('group')].update({'name': r.get('group')})
716 def get_group_resources(self, group):
717 return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
719 def get_groups_resources(self):
720 for g in self.data['groups']:
721 yield g, self.get_group_resources(g)
723 def get_quota(self, group_quotas):
724 print '!!!!!', group_quotas
725 for r, v in group_quotas.iteritems():
727 quota = self.data['resources'].get(rname)
732 def get_policies(self, policies_data):
733 for policy in policies_data:
734 rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
735 policy.update(self.data['resources'].get(rname))
739 return self.data.__repr__()
741 def __iter__(self, *args, **kwargs):
742 return self.data.__iter__(*args, **kwargs)
744 def __getitem__(self, *args, **kwargs):
745 return self.data.__getitem__(*args, **kwargs)
747 def get(self, *args, **kwargs):
748 return self.data.get(*args, **kwargs)
752 @require_http_methods(["GET", "POST"])
753 @signed_terms_required
755 def group_add(request, kind_name='default'):
757 result = callpoint.list_resources()
758 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
759 resource_catalog.update_from_result(result)
761 if not result.is_success:
764 'Unable to retrieve system resources: %s' % result.reason
768 kind = GroupKind.objects.get(name=kind_name)
770 return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
774 post_save_redirect = '/im/group/%(id)s/'
775 context_processors = None
776 model, form_class = get_model_and_form_class(
778 form_class=AstakosGroupCreationForm
781 if request.method == 'POST':
782 form = form_class(request.POST, request.FILES)
784 print '!!!!!!!!!! CLEANED DATA', form.cleaned_data
785 return render_response(
786 template='im/astakosgroup_form_summary.html',
787 context_instance=get_context(request),
788 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
789 policies = resource_catalog.get_policies(form.policies()),
790 resource_catalog= resource_catalog,
798 for group, resources in resource_catalog.get_groups_resources():
799 data['is_selected_%s' % group] = False
800 for resource in resources:
801 data['%s_uplimit' % resource] = ''
803 form = form_class(data)
805 # Create the template, context, response
806 template_name = "%s/%s_form.html" % (
807 model._meta.app_label,
808 model._meta.object_name.lower()
810 t = template_loader.get_template(template_name)
811 c = RequestContext(request, {
814 'resource_catalog':resource_catalog,
815 }, context_processors)
816 return HttpResponse(t.render(c))
819 #@require_http_methods(["POST"])
820 @require_http_methods(["GET", "POST"])
821 @signed_terms_required
823 def group_add_complete(request):
825 form = AstakosGroupCreationSummaryForm(request.POST)
827 d = form.cleaned_data
828 d['owners'] = [request.user]
829 result = callpoint.create_groups((d,)).next()
830 if result.is_success:
831 new_object = result.data[0]
832 msg = _(astakos_messages.OBJECT_CREATED) %\
833 {"verbose_name": model._meta.verbose_name}
834 messages.success(request, msg, fail_silently=True)
838 send_group_creation_notification(
839 template_name='im/group_creation_notification.txt',
842 'owner': request.user,
843 'policies': d.get('policies', [])
846 except SendNotificationError, e:
847 messages.error(request, e, fail_silently=True)
848 post_save_redirect = '/im/group/%(id)s/'
849 return HttpResponseRedirect(post_save_redirect % new_object)
851 d = {"verbose_name": model._meta.verbose_name,
852 "reason":result.reason}
853 msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
854 messages.error(request, msg, fail_silently=True)
855 return render_response(
856 template='im/astakosgroup_form_summary.html',
857 context_instance=get_context(request),
861 #@require_http_methods(["GET"])
862 @require_http_methods(["GET", "POST"])
863 @signed_terms_required
865 def group_list(request):
866 none = request.user.astakos_groups.none()
867 sorting = request.GET.get('sorting')
869 SELECT auth_group.id,
871 im_groupkind.name AS kindname,
873 owner.email AS groupowner,
874 (SELECT COUNT(*) FROM im_membership
875 WHERE group_id = im_astakosgroup.group_ptr_id
876 AND date_joined IS NOT NULL) AS approved_members_num,
878 SELECT date_joined FROM im_membership
879 WHERE group_id = im_astakosgroup.group_ptr_id
880 AND person_id = %s) IS NULL
881 THEN 0 ELSE 1 END) AS membership_status
883 INNER JOIN im_membership ON (
884 im_astakosgroup.group_ptr_id = im_membership.group_id)
885 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
886 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
887 LEFT JOIN im_astakosuser_owner ON (
888 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
889 LEFT JOIN auth_user as owner ON (
890 im_astakosuser_owner.astakosuser_id = owner.id)
891 WHERE im_membership.person_id = %s
892 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)
895 query = query+" ORDER BY %s ASC" %sorting
897 query = query+" ORDER BY groupname ASC"
898 q = AstakosGroup.objects.raw(query)
902 # Create the template, context, response
903 template_name = "%s/%s_list.html" % (
904 q.model._meta.app_label,
905 q.model._meta.object_name.lower()
907 extra_context = dict(
910 sorting=request.GET.get('sorting'),
911 page=request.GET.get('page', 1)
913 return render_response(template_name,
914 context_instance=get_context(request, extra_context)
918 @require_http_methods(["GET", "POST"])
919 @signed_terms_required
921 def group_detail(request, group_id):
922 q = AstakosGroup.objects.select_related().filter(pk=group_id)
924 'is_member': """SELECT CASE WHEN EXISTS(
925 SELECT id FROM im_membership
926 WHERE group_id = im_astakosgroup.group_ptr_id
928 THEN 1 ELSE 0 END""" % request.user.id,
929 'is_owner': """SELECT CASE WHEN EXISTS(
930 SELECT id FROM im_astakosuser_owner
931 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
932 AND astakosuser_id = %s)
933 THEN 1 ELSE 0 END""" % request.user.id,
934 'kindname': """SELECT name FROM im_groupkind
935 WHERE id = im_astakosgroup.kind_id"""})
938 context_processors = None
942 except AstakosGroup.DoesNotExist:
943 raise Http404("No %s found matching the query" % (
944 model._meta.verbose_name))
946 update_form = AstakosGroupUpdateForm(instance=obj)
947 addmembers_form = AddGroupMembersForm()
948 if request.method == 'POST':
951 for k, v in request.POST.iteritems():
952 if k in update_form.fields:
954 if k in addmembers_form.fields:
955 addmembers_data[k] = v
956 update_data = update_data or None
957 addmembers_data = addmembers_data or None
958 update_form = AstakosGroupUpdateForm(update_data, instance=obj)
959 addmembers_form = AddGroupMembersForm(addmembers_data)
960 if update_form.is_valid():
962 if addmembers_form.is_valid():
964 map(obj.approve_member, addmembers_form.valid_users)
965 except AssertionError:
966 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
967 messages.error(request, msg)
968 addmembers_form = AddGroupMembersForm()
970 template_name = "%s/%s_detail.html" % (
971 model._meta.app_label, model._meta.object_name.lower())
972 t = template_loader.get_template(template_name)
973 c = RequestContext(request, {
975 }, context_processors)
978 sorting = request.GET.get('sorting')
980 form = MembersSortForm({'sort_by': sorting})
982 sorting = form.cleaned_data.get('sort_by')
986 result = callpoint.list_resources()
987 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
988 resource_catalog.update_from_result(result)
991 if not result.is_success:
994 'Unable to retrieve system resources: %s' % result.reason
997 print '######', obj.quota
999 extra_context = {'update_form': update_form,
1000 'addmembers_form': addmembers_form,
1001 'page': request.GET.get('page', 1),
1003 'resource_catalog':resource_catalog,
1004 'quota':resource_catalog.get_quota(obj.quota)}
1005 for key, value in extra_context.items():
1010 response = HttpResponse(t.render(c), mimetype=mimetype)
1012 request, response, model, getattr(obj, obj._meta.pk.name))
1016 @require_http_methods(["GET", "POST"])
1017 @signed_terms_required
1019 def group_search(request, extra_context=None, **kwargs):
1020 q = request.GET.get('q')
1021 sorting = request.GET.get('sorting')
1022 if request.method == 'GET':
1023 form = AstakosGroupSearchForm({'q': q} if q else None)
1025 form = AstakosGroupSearchForm(get_query(request))
1027 q = form.cleaned_data['q'].strip()
1029 queryset = AstakosGroup.objects.select_related()
1030 queryset = queryset.filter(name__contains=q)
1031 queryset = queryset.filter(approval_date__isnull=False)
1032 queryset = queryset.extra(select={
1033 'groupname': DB_REPLACE_GROUP_SCHEME,
1034 'kindname': "im_groupkind.name",
1035 'approved_members_num': """
1036 SELECT COUNT(*) FROM im_membership
1037 WHERE group_id = im_astakosgroup.group_ptr_id
1038 AND date_joined IS NOT NULL""",
1039 'membership_approval_date': """
1040 SELECT date_joined FROM im_membership
1041 WHERE group_id = im_astakosgroup.group_ptr_id
1042 AND person_id = %s""" % request.user.id,
1044 SELECT CASE WHEN EXISTS(
1045 SELECT date_joined FROM im_membership
1046 WHERE group_id = im_astakosgroup.group_ptr_id
1048 THEN 1 ELSE 0 END""" % request.user.id,
1050 SELECT CASE WHEN EXISTS(
1051 SELECT id FROM im_astakosuser_owner
1052 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1053 AND astakosuser_id = %s)
1054 THEN 1 ELSE 0 END""" % request.user.id,
1055 'is_owner': """SELECT CASE WHEN EXISTS(
1056 SELECT id FROM im_astakosuser_owner
1057 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1058 AND astakosuser_id = %s)
1059 THEN 1 ELSE 0 END""" % request.user.id,
1062 # TODO check sorting value
1063 queryset = queryset.order_by(sorting)
1065 queryset = queryset.order_by("groupname")
1068 queryset = AstakosGroup.objects.none()
1072 paginate_by=PAGINATE_BY,
1073 page=request.GET.get('page') or 1,
1074 template_name='im/astakosgroup_list.html',
1075 extra_context=dict(form=form,
1081 @require_http_methods(["GET", "POST"])
1082 @signed_terms_required
1084 def group_all(request, extra_context=None, **kwargs):
1085 q = AstakosGroup.objects.select_related()
1086 q = q.filter(approval_date__isnull=False)
1087 q = q.extra(select={
1088 'groupname': DB_REPLACE_GROUP_SCHEME,
1089 'kindname': "im_groupkind.name",
1090 'approved_members_num': """
1091 SELECT COUNT(*) FROM im_membership
1092 WHERE group_id = im_astakosgroup.group_ptr_id
1093 AND date_joined IS NOT NULL""",
1094 'membership_approval_date': """
1095 SELECT date_joined FROM im_membership
1096 WHERE group_id = im_astakosgroup.group_ptr_id
1097 AND person_id = %s""" % request.user.id,
1099 SELECT CASE WHEN EXISTS(
1100 SELECT date_joined FROM im_membership
1101 WHERE group_id = im_astakosgroup.group_ptr_id
1103 THEN 1 ELSE 0 END""" % request.user.id,
1104 'is_owner': """SELECT CASE WHEN EXISTS(
1105 SELECT id FROM im_astakosuser_owner
1106 WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1107 AND astakosuser_id = %s)
1108 THEN 1 ELSE 0 END""" % request.user.id, })
1109 sorting = request.GET.get('sorting')
1111 # TODO check sorting value
1112 q = q.order_by(sorting)
1114 q = q.order_by("groupname")
1119 paginate_by=PAGINATE_BY,
1120 page=request.GET.get('page') or 1,
1121 template_name='im/astakosgroup_list.html',
1122 extra_context=dict(form=AstakosGroupSearchForm(),
1127 #@require_http_methods(["POST"])
1128 @require_http_methods(["POST", "GET"])
1129 @signed_terms_required
1131 def group_join(request, group_id):
1132 m = Membership(group_id=group_id,
1133 person=request.user,
1134 date_requested=datetime.now())
1137 post_save_redirect = reverse(
1139 kwargs=dict(group_id=group_id))
1140 return HttpResponseRedirect(post_save_redirect)
1141 except IntegrityError, e:
1143 msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1144 messages.error(request, msg)
1145 return group_search(request)
1148 @require_http_methods(["POST"])
1149 @signed_terms_required
1151 def group_leave(request, group_id):
1153 m = Membership.objects.select_related().get(
1155 person=request.user)
1156 except Membership.DoesNotExist:
1157 return HttpResponseBadRequest(_(astakos_messages.NOT_A_MEMBER))
1158 if request.user in m.group.owner.all():
1159 return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1160 return delete_object(
1164 template_name='im/astakosgroup_list.html',
1165 post_delete_redirect=reverse(
1167 kwargs=dict(group_id=group_id)))
1170 def handle_membership(func):
1172 def wrapper(request, group_id, user_id):
1174 m = Membership.objects.select_related().get(
1177 except Membership.DoesNotExist:
1178 return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1180 if request.user not in m.group.owner.all():
1181 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1183 return group_detail(request, group_id)
1187 #@require_http_methods(["POST"])
1188 @require_http_methods(["POST", "GET"])
1189 @signed_terms_required
1192 def approve_member(request, membership):
1194 membership.approve()
1195 realname = membership.person.realname
1196 msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1197 messages.success(request, msg)
1198 except AssertionError:
1199 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1200 messages.error(request, msg)
1201 except BaseException, e:
1203 realname = membership.person.realname
1204 msg = _(astakos_messages.GENERIC_ERROR)
1205 messages.error(request, msg)
1208 @signed_terms_required
1211 def disapprove_member(request, membership):
1213 membership.disapprove()
1214 realname = membership.person.realname
1215 msg = MEMBER_REMOVED % realname
1216 messages.success(request, msg)
1217 except BaseException, e:
1219 msg = _(astakos_messages.GENERIC_ERROR)
1220 messages.error(request, msg)
1223 #@require_http_methods(["GET"])
1224 @require_http_methods(["POST", "GET"])
1225 @signed_terms_required
1227 def resource_list(request):
1228 # if request.method == 'POST':
1229 # form = PickResourceForm(request.POST)
1230 # if form.is_valid():
1231 # r = form.cleaned_data.get('resource')
1233 # groups = request.user.membership_set.only('group').filter(
1234 # date_joined__isnull=False)
1235 # groups = [g.group_id for g in groups]
1236 # q = AstakosGroupQuota.objects.select_related().filter(
1237 # resource=r, group__in=groups)
1239 # form = PickResourceForm()
1240 # q = AstakosGroupQuota.objects.none()
1242 # return object_list(request, q,
1243 # template_name='im/astakosuserquota_list.html',
1244 # extra_context={'form': form, 'data':data})
1246 def with_class(entry):
1247 entry['load_class'] = 'red'
1248 max_value = float(entry['maxValue'])
1249 curr_value = float(entry['currValue'])
1251 entry['ratio'] = (curr_value / max_value) * 100
1254 if entry['ratio'] < 66:
1255 entry['load_class'] = 'yellow'
1256 if entry['ratio'] < 33:
1257 entry['load_class'] = 'green'
1260 def pluralize(entry):
1261 entry['plural'] = engine.plural(entry.get('name'))
1264 result = callpoint.get_user_status(request.user.id)
1265 if result.is_success:
1266 backenddata = map(with_class, result.data)
1267 data = map(pluralize, result.data)
1270 messages.error(request, result.reason)
1271 resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1272 resource_catalog.update_from_result_report(result)
1276 return render_response('im/resource_list.html',
1278 context_instance=get_context(request),
1279 resource_catalog=resource_catalog,
1283 def group_create_list(request):
1284 form = PickResourceForm()
1285 return render_response(
1286 template='im/astakosgroup_create_list.html',
1287 context_instance=get_context(request),)
1290 #@require_http_methods(["GET"])
1291 @require_http_methods(["POST", "GET"])
1292 @signed_terms_required
1294 def billing(request):
1296 today = datetime.today()
1297 month_last_day = calendar.monthrange(today.year, today.month)[1]
1298 data['resources'] = map(with_class, data['resources'])
1299 start = request.POST.get('datefrom', None)
1301 today = datetime.fromtimestamp(int(start))
1302 month_last_day = calendar.monthrange(today.year, today.month)[1]
1304 start = datetime(today.year, today.month, 1).strftime("%s")
1305 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1306 r = request_billing.apply(args=('pgerakios@grnet.gr',
1312 status, data = r.result
1313 data = _clear_billing_data(data)
1315 messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1317 messages.error(request, r.result)
1321 return render_response(
1322 template='im/billing.html',
1323 context_instance=get_context(request),
1325 zerodate=datetime(month=1, year=1970, day=1),
1328 month_last_day=month_last_day)
1331 def _clear_billing_data(data):
1333 # remove addcredits entries
1335 return e['serviceName'] != "addcredits"
1338 def servicefilter(service_name):
1339 service = service_name
1342 return e['serviceName'] == service
1345 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1346 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1347 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1348 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1353 #@require_http_methods(["GET"])
1354 @require_http_methods(["POST", "GET"])
1355 @signed_terms_required
1357 def timeline(request):
1358 # data = {'entity':request.user.email}
1360 timeline_header = ()
1361 # form = TimelineForm(data)
1362 form = TimelineForm()
1363 if request.method == 'POST':
1365 form = TimelineForm(data)
1367 data = form.cleaned_data
1368 timeline_header = ('entity', 'resource',
1369 'event name', 'event date',
1370 'incremental cost', 'total cost')
1371 timeline_body = timeline_charge(
1372 data['entity'], data['resource'],
1373 data['start_date'], data['end_date'],
1374 data['details'], data['operation'])
1376 return render_response(template='im/timeline.html',
1377 context_instance=get_context(request),
1379 timeline_header=timeline_header,
1380 timeline_body=timeline_body)