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.
37 from urllib import quote
38 from functools import wraps
39 from datetime import datetime, timedelta
40 from collections import defaultdict
42 from django.contrib import messages
43 from django.contrib.auth.decorators import login_required
44 from django.contrib.auth.views import password_change
45 from django.core.urlresolvers import reverse
46 from django.db import transaction
47 from django.db.models import Q
48 from django.db.utils import IntegrityError
49 from django.forms.fields import URLField
50 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
51 HttpResponseRedirect, HttpResponseBadRequest, Http404
52 from django.shortcuts import redirect
53 from django.template import RequestContext, loader
54 from django.utils.http import urlencode
55 from django.utils.translation import ugettext as _
56 from django.views.generic.create_update import (create_object, delete_object,
57 get_model_and_form_class)
58 from django.views.generic.list_detail import object_list, object_detail
59 from django.http import HttpResponseBadRequest
60 from django.core.paginator import Paginator, InvalidPage
62 from astakos.im.models import (
63 AstakosUser, ApprovalTerms, AstakosGroup, Resource,
64 EmailChange, GroupKind, Membership)
65 from astakos.im.activation_backends import get_backend, SimpleBackend
66 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
67 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
68 FeedbackForm, SignApprovalTermsForm,
69 ExtendedPasswordChangeForm, EmailChangeForm,
70 AstakosGroupCreationForm, AstakosGroupSearchForm,
71 AstakosGroupUpdateForm, AddGroupMembersForm,
72 AstakosGroupSortForm, TimelineForm)
73 from astakos.im.functions import (send_feedback, SendMailError,
74 invite as invite_func, logout as auth_logout,
75 activate as activate_func,
76 switch_account_to_shibboleth,
77 send_admin_notification,
78 SendNotificationError)
79 from astakos.im.endpoints.quotaholder import timeline_charge
80 from astakos.im.settings import (
81 COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
82 LOGGING_LEVEL, PAGINATE_BY)
83 from astakos.im.tasks import request_billing
85 logger = logging.getLogger(__name__)
88 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
91 def render_response(template, tab=None, status=200, reset_cookie=False,
92 context_instance=None, **kwargs):
94 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
95 keyword argument and returns an ``django.http.HttpResponse`` with the
99 tab = template.partition('_')[0].partition('.html')[0]
100 kwargs.setdefault('tab', tab)
101 html = loader.render_to_string(
102 template, kwargs, context_instance=context_instance)
103 response = HttpResponse(html, status=status)
105 set_cookie(response, context_instance['request'].user)
109 def requires_anonymous(func):
111 Decorator checkes whether the request.user is not Anonymous and in that case
112 redirects to `logout`.
115 def wrapper(request, *args):
116 if not request.user.is_anonymous():
117 next = urlencode({'next': request.build_absolute_uri()})
118 logout_uri = reverse(logout) + '?' + next
119 return HttpResponseRedirect(logout_uri)
120 return func(request, *args)
124 def signed_terms_required(func):
126 Decorator checkes whether the request.user is Anonymous and in that case
127 redirects to `logout`.
130 def wrapper(request, *args, **kwargs):
131 if request.user.is_authenticated() and not request.user.signed_terms:
132 params = urlencode({'next': request.build_absolute_uri(),
134 terms_uri = reverse('latest_terms') + '?' + params
135 return HttpResponseRedirect(terms_uri)
136 return func(request, *args, **kwargs)
140 @signed_terms_required
141 def index(request, login_template_name='im/login.html', extra_context=None):
143 If there is logged on user renders the profile page otherwise renders login page.
147 ``login_template_name``
148 A custom login template to use. This is optional; if not specified,
149 this will default to ``im/login.html``.
151 ``profile_template_name``
152 A custom profile template to use. This is optional; if not specified,
153 this will default to ``im/profile.html``.
156 An dictionary of variables to add to the template context.
160 im/profile.html or im/login.html or ``template_name`` keyword argument.
163 template_name = login_template_name
164 if request.user.is_authenticated():
165 return HttpResponseRedirect(reverse('edit_profile'))
166 return render_response(template_name,
167 login_form=LoginForm(request=request),
168 context_instance=get_context(request, extra_context))
172 @signed_terms_required
173 @transaction.commit_manually
174 def invite(request, template_name='im/invitations.html', extra_context=None):
176 Allows a user to invite somebody else.
178 In case of GET request renders a form for providing the invitee information.
179 In case of POST checks whether the user has not run out of invitations and then
180 sends an invitation email to singup to the service.
182 The view uses commit_manually decorator in order to ensure the number of the
183 user invitations is going to be updated only if the email has been successfully sent.
185 If the user isn't logged in, redirects to settings.LOGIN_URL.
190 A custom template to use. This is optional; if not specified,
191 this will default to ``im/invitations.html``.
194 An dictionary of variables to add to the template context.
198 im/invitations.html or ``template_name`` keyword argument.
202 The view expectes the following settings are defined:
204 * LOGIN_URL: login uri
205 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
209 form = InvitationForm()
211 inviter = request.user
212 if request.method == 'POST':
213 form = InvitationForm(request.POST)
214 if inviter.invitations > 0:
217 invitation = form.save()
218 invite_func(invitation, inviter)
219 message = _('Invitation sent to %s' % invitation.username)
220 messages.success(request, message)
221 except SendMailError, e:
223 messages.error(request, message)
224 transaction.rollback()
225 except BaseException, e:
226 message = _('Something went wrong.')
227 messages.error(request, message)
229 transaction.rollback()
233 message = _('No invitations left')
234 messages.error(request, message)
236 sent = [{'email': inv.username,
237 'realname': inv.realname,
238 'is_consumed': inv.is_consumed}
239 for inv in request.user.invitations_sent.all()]
240 kwargs = {'inviter': inviter,
242 context = get_context(request, extra_context, **kwargs)
243 return render_response(template_name,
244 invitation_form=form,
245 context_instance=context)
249 @signed_terms_required
250 def edit_profile(request, template_name='im/profile.html', extra_context=None):
252 Allows a user to edit his/her profile.
254 In case of GET request renders a form for displaying the user information.
255 In case of POST updates the user informantion and redirects to ``next``
256 url parameter if exists.
258 If the user isn't logged in, redirects to settings.LOGIN_URL.
263 A custom template to use. This is optional; if not specified,
264 this will default to ``im/profile.html``.
267 An dictionary of variables to add to the template context.
271 im/profile.html or ``template_name`` keyword argument.
275 The view expectes the following settings are defined:
277 * LOGIN_URL: login uri
279 extra_context = extra_context or {}
280 form = ProfileForm(instance=request.user)
281 extra_context['next'] = request.GET.get('next')
283 if request.method == 'POST':
284 form = ProfileForm(request.POST, instance=request.user)
287 prev_token = request.user.auth_token
289 reset_cookie = user.auth_token != prev_token
290 form = ProfileForm(instance=user)
291 next = request.POST.get('next')
293 return redirect(next)
294 msg = _('Profile has been updated successfully')
295 messages.success(request, msg)
296 except ValueError, ve:
297 messages.success(request, ve)
298 elif request.method == "GET":
299 if not request.user.is_verified:
300 request.user.is_verified = True
302 return render_response(template_name,
303 reset_cookie=reset_cookie,
305 context_instance=get_context(request,
309 @transaction.commit_manually
310 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
312 Allows a user to create a local account.
314 In case of GET request renders a form for entering the user information.
315 In case of POST handles the signup.
317 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
318 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
319 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
320 (see activation_backends);
322 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
323 otherwise renders the same page with a success message.
325 On unsuccessful creation, renders ``template_name`` with an error message.
330 A custom template to render. This is optional;
331 if not specified, this will default to ``im/signup.html``.
334 A custom template to render in case of success. This is optional;
335 if not specified, this will default to ``im/signup_complete.html``.
338 An dictionary of variables to add to the template context.
342 im/signup.html or ``template_name`` keyword argument.
343 im/signup_complete.html or ``on_success`` keyword argument.
345 if request.user.is_authenticated():
346 return HttpResponseRedirect(reverse('edit_profile'))
348 provider = get_query(request).get('provider', 'local')
351 backend = get_backend(request)
352 form = backend.get_signup_form(provider)
354 form = SimpleBackend(request).get_signup_form(provider)
355 messages.error(request, e)
356 if request.method == 'POST':
358 user = form.save(commit=False)
360 result = backend.handle_activation(user)
361 status = messages.SUCCESS
362 message = result.message
364 if 'additional_email' in form.cleaned_data:
365 additional_email = form.cleaned_data['additional_email']
366 if additional_email != user.email:
367 user.additionalmail_set.create(email=additional_email)
368 msg = 'Additional email: %s saved for user %s.' % (
369 additional_email, user.email)
370 logger.log(LOGGING_LEVEL, msg)
371 if user and user.is_active:
372 next = request.POST.get('next', '')
374 return prepare_response(request, user, next=next)
375 messages.add_message(request, status, message)
377 return render_response(on_success,
378 context_instance=get_context(request, extra_context))
379 except SendMailError, e:
381 messages.error(request, message)
382 transaction.rollback()
383 except BaseException, e:
384 message = _('Something went wrong.')
385 messages.error(request, message)
387 transaction.rollback()
388 return render_response(template_name,
391 context_instance=get_context(request, extra_context))
395 @signed_terms_required
396 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
398 Allows a user to send feedback.
400 In case of GET request renders a form for providing the feedback information.
401 In case of POST sends an email to support team.
403 If the user isn't logged in, redirects to settings.LOGIN_URL.
408 A custom template to use. This is optional; if not specified,
409 this will default to ``im/feedback.html``.
412 An dictionary of variables to add to the template context.
416 im/signup.html or ``template_name`` keyword argument.
420 * LOGIN_URL: login uri
421 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
423 if request.method == 'GET':
424 form = FeedbackForm()
425 if request.method == 'POST':
427 return HttpResponse('Unauthorized', status=401)
429 form = FeedbackForm(request.POST)
431 msg = form.cleaned_data['feedback_msg']
432 data = form.cleaned_data['feedback_data']
434 send_feedback(msg, data, request.user, email_template_name)
435 except SendMailError, e:
436 messages.error(request, message)
438 message = _('Feedback successfully sent')
439 messages.success(request, message)
440 return render_response(template_name,
442 context_instance=get_context(request, extra_context))
445 @signed_terms_required
446 def logout(request, template='registration/logged_out.html', extra_context=None):
448 Wraps `django.contrib.auth.logout` and delete the cookie.
450 response = HttpResponse()
451 if request.user.is_authenticated():
452 email = request.user.email
454 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
455 msg = 'Cookie deleted for %s' % email
456 logger.log(LOGGING_LEVEL, msg)
457 next = request.GET.get('next')
459 response['Location'] = next
460 response.status_code = 302
463 response['Location'] = LOGOUT_NEXT
464 response.status_code = 301
466 messages.success(request, _('You have successfully logged out.'))
467 context = get_context(request, extra_context)
468 response.write(loader.render_to_string(template, context_instance=context))
472 @transaction.commit_manually
473 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
475 Activates the user identified by the ``auth`` request parameter, sends a welcome email
476 and renews the user token.
478 The view uses commit_manually decorator in order to ensure the user state will be updated
479 only if the email will be send successfully.
481 token = request.GET.get('auth')
482 next = request.GET.get('next')
484 user = AstakosUser.objects.get(auth_token=token)
485 except AstakosUser.DoesNotExist:
486 return HttpResponseBadRequest(_('No such user'))
489 message = _('Account already active.')
490 messages.error(request, message)
491 return index(request)
494 local_user = AstakosUser.objects.get(
499 except AstakosUser.DoesNotExist:
503 greeting_email_template_name,
504 helpdesk_email_template_name,
507 response = prepare_response(request, user, next, renew=True)
510 except SendMailError, e:
512 messages.error(request, message)
513 transaction.rollback()
514 return index(request)
515 except BaseException, e:
516 message = _('Something went wrong.')
517 messages.error(request, message)
519 transaction.rollback()
520 return index(request)
523 user = switch_account_to_shibboleth(
526 greeting_email_template_name
528 response = prepare_response(request, user, next, renew=True)
531 except SendMailError, e:
533 messages.error(request, message)
534 transaction.rollback()
535 return index(request)
536 except BaseException, e:
537 message = _('Something went wrong.')
538 messages.error(request, message)
540 transaction.rollback()
541 return index(request)
544 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
549 term = ApprovalTerms.objects.order_by('-id')[0]
554 term = ApprovalTerms.objects.get(id=term_id)
555 except ApprovalTerms.DoesNotExist, e:
559 return HttpResponseRedirect(reverse('index'))
560 f = open(term.location, 'r')
563 if request.method == 'POST':
564 next = request.POST.get('next')
566 next = reverse('index')
567 form = SignApprovalTermsForm(request.POST, instance=request.user)
568 if not form.is_valid():
569 return render_response(template_name,
571 approval_terms_form=form,
572 context_instance=get_context(request, extra_context))
574 return HttpResponseRedirect(next)
577 if request.user.is_authenticated() and not request.user.signed_terms:
578 form = SignApprovalTermsForm(instance=request.user)
579 return render_response(template_name,
581 approval_terms_form=form,
582 context_instance=get_context(request, extra_context))
585 @signed_terms_required
586 def change_password(request):
587 return password_change(request,
588 post_change_redirect=reverse('edit_profile'),
589 password_change_form=ExtendedPasswordChangeForm)
592 @signed_terms_required
594 @transaction.commit_manually
595 def change_email(request, activation_key=None,
596 email_template_name='registration/email_change_email.txt',
597 form_template_name='registration/email_change_form.html',
598 confirm_template_name='registration/email_change_done.html',
602 user = EmailChange.objects.change_email(activation_key)
603 if request.user.is_authenticated() and request.user == user:
604 msg = _('Email changed successfully.')
605 messages.success(request, msg)
607 response = prepare_response(request, user)
610 except ValueError, e:
611 messages.error(request, e)
612 return render_response(confirm_template_name,
613 modified_user=user if 'user' in locals(
615 context_instance=get_context(request,
618 if not request.user.is_authenticated():
619 path = quote(request.get_full_path())
620 url = request.build_absolute_uri(reverse('index'))
621 return HttpResponseRedirect(url + '?next=' + path)
622 form = EmailChangeForm(request.POST or None)
623 if request.method == 'POST' and form.is_valid():
625 ec = form.save(email_template_name, request)
626 except SendMailError, e:
628 messages.error(request, msg)
629 transaction.rollback()
630 except IntegrityError, e:
631 msg = _('There is already a pending change email request.')
632 messages.error(request, msg)
634 msg = _('Change email request has been registered succefully.\
635 You are going to receive a verification email in the new address.')
636 messages.success(request, msg)
638 return render_response(form_template_name,
640 context_instance=get_context(request,
644 @signed_terms_required
646 def group_add(request, kind_name='default'):
648 kind = GroupKind.objects.get(name=kind_name)
650 return HttpResponseBadRequest(_('No such group kind'))
652 template_loader = loader
653 post_save_redirect = '/im/group/%(id)s/'
654 context_processors = None
655 model, form_class = get_model_and_form_class(
657 form_class=AstakosGroupCreationForm
660 (str(r.id), r) for r in Resource.objects.select_related().all())
662 if request.method == 'POST':
663 form = form_class(request.POST, request.FILES, resources=resources)
665 new_object = form.save()
668 new_object.owners = [request.user]
670 # save quota policies
671 for (rid, uplimit) in form.resources():
676 # TODO Should I stay or should I go???
679 new_object.astakosgroupquota_set.create(
683 policies.append('%s %d' % (r, uplimit))
684 msg = _("The %(verbose_name)s was created successfully.") %\
685 {"verbose_name": model._meta.verbose_name}
686 messages.success(request, msg, fail_silently=True)
690 send_admin_notification(
691 template_name='im/group_creation_notification.txt',
694 'owner': request.user,
695 'policies': policies,
697 subject='%s alpha2 testing group creation notification' % SITENAME
699 except SendNotificationError, e:
700 messages.error(request, e, fail_silently=True)
701 return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
707 form = form_class(data, resources=resources)
709 # Create the template, context, response
710 template_name = "%s/%s_form.html" % (
711 model._meta.app_label,
712 model._meta.object_name.lower()
714 t = template_loader.get_template(template_name)
715 c = RequestContext(request, {
718 }, context_processors)
719 return HttpResponse(t.render(c))
722 @signed_terms_required
724 def group_list(request):
725 none = request.user.astakos_groups.none()
726 q = AstakosGroup.objects.raw("""
727 SELECT auth_group.id,
729 im_groupkind.name AS kindname,
731 owner.email AS groupowner,
732 (SELECT COUNT(*) FROM im_membership
733 WHERE group_id = im_astakosgroup.group_ptr_id
734 AND date_joined IS NOT NULL) AS approved_members_num,
735 (SELECT date_joined FROM im_membership
736 WHERE group_id = im_astakosgroup.group_ptr_id
737 AND person_id = %s) AS membership_approval_date
739 INNER JOIN im_membership ON (
740 im_astakosgroup.group_ptr_id = im_membership.group_id)
741 INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
742 INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
743 LEFT JOIN im_astakosuser_owner ON (
744 im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
745 LEFT JOIN auth_user as owner ON (
746 im_astakosuser_owner.astakosuser_id = owner.id)
747 WHERE im_membership.person_id = %s
748 """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
749 d = defaultdict(list)
751 if request.user.email == g.groupowner:
755 d.setdefault('own', [])
756 d.setdefault('other', [])
757 for k, l in d.iteritems():
758 page = request.GET.get('%s_page' % k, 1)
759 sorting = globals()['%s_sorting' % k] = request.GET.get('%s_sorting' % k)
761 sort_form = AstakosGroupSortForm({'sort_by': sorting})
762 if sort_form.is_valid():
763 l.sort(key=lambda i: getattr(i, sorting))
764 globals()['%s_sorting' % k] = sorting
765 paginator = Paginator(l, PAGINATE_BY)
768 page_number = int(page)
771 page_number = paginator.num_pages
773 # Page is not 'last', nor can it be converted to an int.
776 page_obj = globals()['%s_page_obj' % k] = paginator.page(page_number)
779 return object_list(request, queryset=none,
780 extra_context={'is_search':False,
781 'mine': own_page_obj,
782 'other': other_page_obj,
783 'own_sorting': own_sorting,
784 'other_sorting': other_sorting
788 @signed_terms_required
790 def group_detail(request, group_id):
792 group = AstakosGroup.objects.select_related().get(id=group_id)
793 except AstakosGroup.DoesNotExist:
794 return HttpResponseBadRequest(_('Invalid group.'))
795 form = AstakosGroupUpdateForm(instance=group)
796 search_form = AddGroupMembersForm()
797 return object_detail(request,
798 AstakosGroup.objects.all(),
800 extra_context={'quota': group.quota,
802 'search_form': search_form}
806 @signed_terms_required
808 def group_update(request, group_id):
809 if request.method != 'POST':
810 return HttpResponseBadRequest('Method not allowed.')
812 group = AstakosGroup.objects.select_related().get(id=group_id)
813 except AstakosGroup.DoesNotExist:
814 return HttpResponseBadRequest(_('Invalid group.'))
815 form = AstakosGroupUpdateForm(request.POST, instance=group)
818 search_form = AddGroupMembersForm()
819 return object_detail(request,
820 AstakosGroup.objects.all(),
822 extra_context={'quota': group.quota,
824 'search_form': search_form})
826 @signed_terms_required
828 def group_search(request, extra_context=None, **kwargs):
829 q = request.GET.get('q')
830 if request.method == 'GET':
831 form = AstakosGroupSearchForm({'q': q} if q else None)
833 form = AstakosGroupSearchForm(get_query(request))
835 q = form.cleaned_data['q'].strip()
837 queryset = AstakosGroup.objects.select_related()
838 queryset = queryset.filter(name__contains=q)
839 queryset = queryset.filter(approval_date__isnull=False)
840 queryset = queryset.extra(select={
841 'groupname': DB_REPLACE_GROUP_SCHEME,
842 'kindname': "im_groupkind.name",
843 'approved_members_num': """
844 SELECT COUNT(*) FROM im_membership
845 WHERE group_id = im_astakosgroup.group_ptr_id
846 AND date_joined IS NOT NULL""",
847 'membership_approval_date': """
848 SELECT date_joined FROM im_membership
849 WHERE group_id = im_astakosgroup.group_ptr_id
850 AND person_id = %s""" % request.user.id,
852 SELECT CASE WHEN EXISTS(
853 SELECT date_joined FROM im_membership
854 WHERE group_id = im_astakosgroup.group_ptr_id
856 THEN 1 ELSE 0 END""" % request.user.id})
858 queryset = AstakosGroup.objects.none()
862 paginate_by=PAGINATE_BY,
863 page=request.GET.get('page') or 1,
864 template_name='im/astakosgroup_list.html',
865 extra_context=dict(form=form,
869 @signed_terms_required
871 def group_all(request, extra_context=None, **kwargs):
872 q = AstakosGroup.objects.select_related()
873 q = q.filter(approval_date__isnull=False)
875 'groupname': DB_REPLACE_GROUP_SCHEME,
876 'kindname': "im_groupkind.name",
877 'approved_members_num': """
878 SELECT COUNT(*) FROM im_membership
879 WHERE group_id = im_astakosgroup.group_ptr_id
880 AND date_joined IS NOT NULL""",
881 'membership_approval_date': """
882 SELECT date_joined FROM im_membership
883 WHERE group_id = im_astakosgroup.group_ptr_id
884 AND person_id = %s""" % request.user.id,
886 SELECT CASE WHEN EXISTS(
887 SELECT date_joined FROM im_membership
888 WHERE group_id = im_astakosgroup.group_ptr_id
890 THEN 1 ELSE 0 END""" % request.user.id})
894 paginate_by=PAGINATE_BY,
895 page=request.GET.get('page') or 1,
896 template_name='im/astakosgroup_list.html',
897 extra_context=dict(form=AstakosGroupSearchForm(),
901 @signed_terms_required
903 def group_join(request, group_id):
904 m = Membership(group_id=group_id,
906 date_requested=datetime.now())
909 post_save_redirect = reverse(
911 kwargs=dict(group_id=group_id))
912 return HttpResponseRedirect(post_save_redirect)
913 except IntegrityError, e:
915 msg = _('Failed to join group.')
916 messages.error(request, msg)
917 return group_search(request)
920 @signed_terms_required
922 def group_leave(request, group_id):
924 m = Membership.objects.select_related().get(
928 except Membership.DoesNotExist:
929 return HttpResponseBadRequest(_('Invalid membership.'))
930 if request.user in m.group.owner.all():
931 return HttpResponseForbidden(_('Owner can not leave the group.'))
932 return delete_object(
936 template_name='im/astakosgroup_list.html',
937 post_delete_redirect=reverse(
939 kwargs=dict(group_id=group_id)
944 def handle_membership(func):
946 def wrapper(request, group_id, user_id):
948 m = Membership.objects.select_related().get(
952 except Membership.DoesNotExist:
953 return HttpResponseBadRequest(_('Invalid membership.'))
955 if request.user not in m.group.owner.all():
956 return HttpResponseForbidden(_('User is not a group owner.'))
958 return render_response(
959 template='im/astakosgroup_detail.html',
960 context_instance=get_context(request),
967 @signed_terms_required
970 def approve_member(request, membership):
973 realname = membership.person.realname
974 msg = _('%s has been successfully joined the group.' % realname)
975 messages.success(request, msg)
976 except BaseException, e:
978 msg = _('Something went wrong during %s\'s approval.' % realname)
979 messages.error(request, msg)
982 @signed_terms_required
985 def disapprove_member(request, membership):
987 membership.disapprove()
988 realname = membership.person.realname
989 msg = _('%s has been successfully removed from the group.' % realname)
990 messages.success(request, msg)
991 except BaseException, e:
993 msg = _('Something went wrong during %s\'s disapproval.' % realname)
994 messages.error(request, msg)
999 @signed_terms_required
1001 def add_members(request, group_id):
1002 if request.method != 'POST':
1003 return HttpResponseBadRequest(_('Bad method'))
1005 group = AstakosGroup.objects.select_related().get(id=group_id)
1006 except AstakosGroup.DoesNotExist:
1007 return HttpResponseBadRequest(_('Invalid group.'))
1008 search_form = AddGroupMembersForm(request.POST)
1009 if search_form.is_valid():
1010 users = search_form.get_valid_users()
1011 map(group.approve_member, users)
1012 search_form = AddGroupMembersForm()
1013 form = AstakosGroupUpdateForm(instance=group)
1014 return object_detail(request,
1015 AstakosGroup.objects.all(),
1017 extra_context={'quota': group.quota,
1019 'search_form' : search_form})
1022 @signed_terms_required
1024 def resource_list(request):
1025 return render_response(
1026 template='im/astakosuserquota_list.html',
1027 context_instance=get_context(request),
1028 quota=request.user.quota
1032 def group_create_list(request):
1033 return render_response(
1034 template='im/astakosgroup_create_list.html',
1035 context_instance=get_context(request),
1039 @signed_terms_required
1041 def billing(request):
1043 today = datetime.today()
1044 month_last_day= calendar.monthrange(today.year, today.month)[1]
1046 start = request.POST.get('datefrom', None)
1048 today = datetime.fromtimestamp(int(start))
1049 month_last_day= calendar.monthrange(today.year, today.month)[1]
1051 start = datetime(today.year, today.month, 1).strftime("%s")
1052 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1053 r = request_billing.apply(args=('pgerakios@grnet.gr',
1059 status, data = r.result
1060 data=clear_billing_data(data)
1062 messages.error(request, _('Service response status: %d' % status))
1064 messages.error(request, r.result)
1068 return render_response(
1069 template='im/billing.html',
1070 context_instance=get_context(request),
1072 zerodate=datetime(month=1,year=1970, day=1),
1075 month_last_day=month_last_day)
1077 def clear_billing_data(data):
1079 # remove addcredits entries
1081 return e['serviceName'] != "addcredits"
1086 def servicefilter(service_name):
1087 service = service_name
1089 return e['serviceName'] == service
1093 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1094 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1095 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1096 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1100 @signed_terms_required
1102 def timeline(request):
1103 # data = {'entity':request.user.email}
1105 timeline_header = ()
1106 # form = TimelineForm(data)
1107 form = TimelineForm()
1108 if request.method == 'POST':
1110 form = TimelineForm(data)
1112 data = form.cleaned_data
1113 timeline_header = ('entity', 'resource',
1114 'event name', 'event date',
1115 'incremental cost', 'total cost')
1116 timeline_body = timeline_charge(
1117 data['entity'], data['resource'],
1118 data['start_date'], data['end_date'],
1119 data['details'], data['operation'])
1121 return render_response(template='im/timeline.html',
1122 context_instance=get_context(request),
1124 timeline_header=timeline_header,
1125 timeline_body=timeline_body)