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:
756 for k, l in d.iteritems():
757 page = request.GET.get('%s_page' % k, 1)
758 sorting = globals()['%s_sorting' % k] = request.GET.get('%s_sorting' % k)
760 sort_form = AstakosGroupSortForm({'sort_by': sorting})
761 if sort_form.is_valid():
762 l.sort(key=lambda i: getattr(i, sorting))
763 globals()['%s_sorting' % k] = sorting
764 paginator = Paginator(l, PAGINATE_BY)
767 page_number = int(page)
770 page_number = paginator.num_pages
772 # Page is not 'last', nor can it be converted to an int.
775 page_obj = globals()['%s_page_obj' % k] = paginator.page(page_number)
778 return object_list(request, queryset=none,
779 extra_context={'is_search':False,
780 'mine': own_page_obj,
781 'other': other_page_obj,
782 'own_sorting': own_sorting,
783 'other_sorting': other_sorting
787 @signed_terms_required
789 def group_detail(request, group_id):
791 group = AstakosGroup.objects.select_related().get(id=group_id)
792 except AstakosGroup.DoesNotExist:
793 return HttpResponseBadRequest(_('Invalid group.'))
794 form = AstakosGroupUpdateForm(instance=group)
795 search_form = AddGroupMembersForm()
796 return object_detail(request,
797 AstakosGroup.objects.all(),
799 extra_context={'quota': group.quota,
801 'search_form': search_form}
805 @signed_terms_required
807 def group_update(request, group_id):
808 if request.method != 'POST':
809 return HttpResponseBadRequest('Method not allowed.')
811 group = AstakosGroup.objects.select_related().get(id=group_id)
812 except AstakosGroup.DoesNotExist:
813 return HttpResponseBadRequest(_('Invalid group.'))
814 form = AstakosGroupUpdateForm(request.POST, instance=group)
817 search_form = AddGroupMembersForm()
818 return object_detail(request,
819 AstakosGroup.objects.all(),
821 extra_context={'quota': group.quota,
823 'search_form': search_form})
825 @signed_terms_required
827 def group_search(request, extra_context=None, **kwargs):
828 q = request.GET.get('q')
829 if request.method == 'GET':
830 form = AstakosGroupSearchForm({'q': q} if q else None)
832 form = AstakosGroupSearchForm(get_query(request))
834 q = form.cleaned_data['q'].strip()
836 queryset = AstakosGroup.objects.select_related()
837 queryset = queryset.filter(name__contains=q)
838 queryset = queryset.filter(approval_date__isnull=False)
839 queryset = queryset.extra(select={
840 'groupname': DB_REPLACE_GROUP_SCHEME,
841 'kindname': "im_groupkind.name",
842 'approved_members_num': """
843 SELECT COUNT(*) FROM im_membership
844 WHERE group_id = im_astakosgroup.group_ptr_id
845 AND date_joined IS NOT NULL""",
846 'membership_approval_date': """
847 SELECT date_joined FROM im_membership
848 WHERE group_id = im_astakosgroup.group_ptr_id
849 AND person_id = %s""" % request.user.id,
851 SELECT CASE WHEN EXISTS(
852 SELECT date_joined FROM im_membership
853 WHERE group_id = im_astakosgroup.group_ptr_id
855 THEN 1 ELSE 0 END""" % request.user.id})
857 queryset = AstakosGroup.objects.none()
861 paginate_by=PAGINATE_BY,
862 page=request.GET.get('page') or 1,
863 template_name='im/astakosgroup_list.html',
864 extra_context=dict(form=form,
868 @signed_terms_required
870 def group_all(request, extra_context=None, **kwargs):
871 q = AstakosGroup.objects.select_related()
872 q = q.filter(approval_date__isnull=False)
874 'groupname': DB_REPLACE_GROUP_SCHEME,
875 'kindname': "im_groupkind.name",
876 'approved_members_num': """
877 SELECT COUNT(*) FROM im_membership
878 WHERE group_id = im_astakosgroup.group_ptr_id
879 AND date_joined IS NOT NULL""",
880 'membership_approval_date': """
881 SELECT date_joined FROM im_membership
882 WHERE group_id = im_astakosgroup.group_ptr_id
883 AND person_id = %s""" % request.user.id,
885 SELECT CASE WHEN EXISTS(
886 SELECT date_joined FROM im_membership
887 WHERE group_id = im_astakosgroup.group_ptr_id
889 THEN 1 ELSE 0 END""" % request.user.id})
893 paginate_by=PAGINATE_BY,
894 page=request.GET.get('page') or 1,
895 template_name='im/astakosgroup_list.html',
896 extra_context=dict(form=AstakosGroupSearchForm(),
900 @signed_terms_required
902 def group_join(request, group_id):
903 m = Membership(group_id=group_id,
905 date_requested=datetime.now())
908 post_save_redirect = reverse(
910 kwargs=dict(group_id=group_id))
911 return HttpResponseRedirect(post_save_redirect)
912 except IntegrityError, e:
914 msg = _('Failed to join group.')
915 messages.error(request, msg)
916 return group_search(request)
919 @signed_terms_required
921 def group_leave(request, group_id):
923 m = Membership.objects.select_related().get(
927 except Membership.DoesNotExist:
928 return HttpResponseBadRequest(_('Invalid membership.'))
929 if request.user in m.group.owner.all():
930 return HttpResponseForbidden(_('Owner can not leave the group.'))
931 return delete_object(
935 template_name='im/astakosgroup_list.html',
936 post_delete_redirect=reverse(
938 kwargs=dict(group_id=group_id)
943 def handle_membership(func):
945 def wrapper(request, group_id, user_id):
947 m = Membership.objects.select_related().get(
951 except Membership.DoesNotExist:
952 return HttpResponseBadRequest(_('Invalid membership.'))
954 if request.user not in m.group.owner.all():
955 return HttpResponseForbidden(_('User is not a group owner.'))
957 return render_response(
958 template='im/astakosgroup_detail.html',
959 context_instance=get_context(request),
966 @signed_terms_required
969 def approve_member(request, membership):
972 realname = membership.person.realname
973 msg = _('%s has been successfully joined the group.' % realname)
974 messages.success(request, msg)
975 except BaseException, e:
977 msg = _('Something went wrong during %s\'s approval.' % realname)
978 messages.error(request, msg)
981 @signed_terms_required
984 def disapprove_member(request, membership):
986 membership.disapprove()
987 realname = membership.person.realname
988 msg = _('%s has been successfully removed from the group.' % realname)
989 messages.success(request, msg)
990 except BaseException, e:
992 msg = _('Something went wrong during %s\'s disapproval.' % realname)
993 messages.error(request, msg)
998 @signed_terms_required
1000 def add_members(request, group_id):
1001 if request.method != 'POST':
1002 return HttpResponseBadRequest(_('Bad method'))
1004 group = AstakosGroup.objects.select_related().get(id=group_id)
1005 except AstakosGroup.DoesNotExist:
1006 return HttpResponseBadRequest(_('Invalid group.'))
1007 search_form = AddGroupMembersForm(request.POST)
1008 if search_form.is_valid():
1009 users = search_form.get_valid_users()
1010 map(group.approve_member, users)
1011 search_form = AddGroupMembersForm()
1012 form = AstakosGroupUpdateForm(instance=group)
1013 return object_detail(request,
1014 AstakosGroup.objects.all(),
1016 extra_context={'quota': group.quota,
1018 'search_form' : search_form})
1021 @signed_terms_required
1023 def resource_list(request):
1024 return render_response(
1025 template='im/astakosuserquota_list.html',
1026 context_instance=get_context(request),
1027 quota=request.user.quota
1031 def group_create_list(request):
1032 return render_response(
1033 template='im/astakosgroup_create_list.html',
1034 context_instance=get_context(request),
1038 @signed_terms_required
1040 def billing(request):
1042 today = datetime.today()
1043 month_last_day= calendar.monthrange(today.year, today.month)[1]
1045 start = request.POST.get('datefrom', None)
1047 today = datetime.fromtimestamp(int(start))
1048 month_last_day= calendar.monthrange(today.year, today.month)[1]
1050 start = datetime(today.year, today.month, 1).strftime("%s")
1051 end = datetime(today.year, today.month, month_last_day).strftime("%s")
1052 r = request_billing.apply(args=('pgerakios@grnet.gr',
1058 status, data = r.result
1059 data=clear_billing_data(data)
1061 messages.error(request, _('Service response status: %d' % status))
1063 messages.error(request, r.result)
1067 return render_response(
1068 template='im/billing.html',
1069 context_instance=get_context(request),
1071 zerodate=datetime(month=1,year=1970, day=1),
1074 month_last_day=month_last_day)
1076 def clear_billing_data(data):
1078 # remove addcredits entries
1080 return e['serviceName'] != "addcredits"
1085 def servicefilter(service_name):
1086 service = service_name
1088 return e['serviceName'] == service
1092 data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1093 data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1094 data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1095 data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1099 @signed_terms_required
1101 def timeline(request):
1102 data = {'entity':request.user.email}
1104 timeline_header = ()
1105 form = TimelineForm(data)
1106 if request.method == 'POST':
1108 form = TimelineForm(data)
1110 data = form.cleaned_data
1111 timeline_header = ('entity', 'resource',
1112 'event name', 'event date',
1113 'incremental cost', 'total cost')
1114 timeline_body = timeline_charge(
1115 data['entity'], data['resource'],
1116 data['start_date'], data['end_date'],
1117 data['details'], data['operation'])
1119 return render_response(template='im/timeline.html',
1120 context_instance=get_context(request),
1122 timeline_header=timeline_header,
1123 timeline_body=timeline_body)