Reduce database interaction in group_detail
[astakos] / snf-astakos-app / astakos / im / views.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
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.
15 #
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.
28 #
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.
33
34 import logging
35 import calendar
36
37 from urllib import quote
38 from functools import wraps
39 from datetime import datetime, timedelta
40 from collections import defaultdict
41
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.db.models.fields import DateTimeField
51 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
52     HttpResponseRedirect, HttpResponseBadRequest, Http404
53 from django.shortcuts import redirect
54 from django.template import RequestContext, loader as template_loader
55 from django.utils.http import urlencode
56 from django.utils.translation import ugettext as _
57 from django.views.generic.create_update import (create_object, delete_object,
58                                                 get_model_and_form_class)
59 from django.views.generic.list_detail import object_list, object_detail
60 from django.http import HttpResponseBadRequest
61 from django.core.paginator import Paginator, InvalidPage
62 from django.core.xheaders import populate_xheaders
63
64 from astakos.im.models import (
65     AstakosUser, ApprovalTerms, AstakosGroup, Resource,
66     EmailChange, GroupKind, Membership)
67 from astakos.im.activation_backends import get_backend, SimpleBackend
68 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
69 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
70                               FeedbackForm, SignApprovalTermsForm,
71                               ExtendedPasswordChangeForm, EmailChangeForm,
72                               AstakosGroupCreationForm, AstakosGroupSearchForm,
73                               AstakosGroupUpdateForm, AddGroupMembersForm,
74                               AstakosGroupSortForm)
75 from astakos.im.functions import (send_feedback, SendMailError,
76                                   invite as invite_func, logout as auth_logout,
77                                   activate as activate_func,
78                                   switch_account_to_shibboleth,
79                                   send_admin_notification,
80                                   SendNotificationError)
81 from astakos.im.settings import (
82     COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
83     LOGGING_LEVEL, PAGINATE_BY)
84 from astakos.im.tasks import request_billing
85
86 logger = logging.getLogger(__name__)
87
88
89 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
90                                      'https://', '')"""
91
92 def render_response(template, tab=None, status=200, reset_cookie=False,
93                     context_instance=None, **kwargs):
94     """
95     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
96     keyword argument and returns an ``django.http.HttpResponse`` with the
97     specified ``status``.
98     """
99     if tab is None:
100         tab = template.partition('_')[0].partition('.html')[0]
101     kwargs.setdefault('tab', tab)
102     html = template_loader.render_to_string(
103         template, kwargs, context_instance=context_instance)
104     response = HttpResponse(html, status=status)
105     if reset_cookie:
106         set_cookie(response, context_instance['request'].user)
107     return response
108
109
110 def requires_anonymous(func):
111     """
112     Decorator checkes whether the request.user is not Anonymous and in that case
113     redirects to `logout`.
114     """
115     @wraps(func)
116     def wrapper(request, *args):
117         if not request.user.is_anonymous():
118             next = urlencode({'next': request.build_absolute_uri()})
119             logout_uri = reverse(logout) + '?' + next
120             return HttpResponseRedirect(logout_uri)
121         return func(request, *args)
122     return wrapper
123
124
125 def signed_terms_required(func):
126     """
127     Decorator checkes whether the request.user is Anonymous and in that case
128     redirects to `logout`.
129     """
130     @wraps(func)
131     def wrapper(request, *args, **kwargs):
132         if request.user.is_authenticated() and not request.user.signed_terms:
133             params = urlencode({'next': request.build_absolute_uri(),
134                                 'show_form': ''})
135             terms_uri = reverse('latest_terms') + '?' + params
136             return HttpResponseRedirect(terms_uri)
137         return func(request, *args, **kwargs)
138     return wrapper
139
140
141 @signed_terms_required
142 def index(request, login_template_name='im/login.html', extra_context=None):
143     """
144     If there is logged on user renders the profile page otherwise renders login page.
145
146     **Arguments**
147
148     ``login_template_name``
149         A custom login template to use. This is optional; if not specified,
150         this will default to ``im/login.html``.
151
152     ``profile_template_name``
153         A custom profile template to use. This is optional; if not specified,
154         this will default to ``im/profile.html``.
155
156     ``extra_context``
157         An dictionary of variables to add to the template context.
158
159     **Template:**
160
161     im/profile.html or im/login.html or ``template_name`` keyword argument.
162
163     """
164     template_name = login_template_name
165     if request.user.is_authenticated():
166         return HttpResponseRedirect(reverse('edit_profile'))
167     return render_response(template_name,
168                            login_form=LoginForm(request=request),
169                            context_instance=get_context(request, extra_context))
170
171
172 @login_required
173 @signed_terms_required
174 @transaction.commit_manually
175 def invite(request, template_name='im/invitations.html', extra_context=None):
176     """
177     Allows a user to invite somebody else.
178
179     In case of GET request renders a form for providing the invitee information.
180     In case of POST checks whether the user has not run out of invitations and then
181     sends an invitation email to singup to the service.
182
183     The view uses commit_manually decorator in order to ensure the number of the
184     user invitations is going to be updated only if the email has been successfully sent.
185
186     If the user isn't logged in, redirects to settings.LOGIN_URL.
187
188     **Arguments**
189
190     ``template_name``
191         A custom template to use. This is optional; if not specified,
192         this will default to ``im/invitations.html``.
193
194     ``extra_context``
195         An dictionary of variables to add to the template context.
196
197     **Template:**
198
199     im/invitations.html or ``template_name`` keyword argument.
200
201     **Settings:**
202
203     The view expectes the following settings are defined:
204
205     * LOGIN_URL: login uri
206     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
207     """
208     status = None
209     message = None
210     form = InvitationForm()
211
212     inviter = request.user
213     if request.method == 'POST':
214         form = InvitationForm(request.POST)
215         if inviter.invitations > 0:
216             if form.is_valid():
217                 try:
218                     invitation = form.save()
219                     invite_func(invitation, inviter)
220                     message = _('Invitation sent to %s' % invitation.username)
221                     messages.success(request, message)
222                 except SendMailError, e:
223                     message = e.message
224                     messages.error(request, message)
225                     transaction.rollback()
226                 except BaseException, e:
227                     message = _('Something went wrong.')
228                     messages.error(request, message)
229                     logger.exception(e)
230                     transaction.rollback()
231                 else:
232                     transaction.commit()
233         else:
234             message = _('No invitations left')
235             messages.error(request, message)
236
237     sent = [{'email': inv.username,
238              'realname': inv.realname,
239              'is_consumed': inv.is_consumed}
240             for inv in request.user.invitations_sent.all()]
241     kwargs = {'inviter': inviter,
242               'sent': sent}
243     context = get_context(request, extra_context, **kwargs)
244     return render_response(template_name,
245                            invitation_form=form,
246                            context_instance=context)
247
248
249 @login_required
250 @signed_terms_required
251 def edit_profile(request, template_name='im/profile.html', extra_context=None):
252     """
253     Allows a user to edit his/her profile.
254
255     In case of GET request renders a form for displaying the user information.
256     In case of POST updates the user informantion and redirects to ``next``
257     url parameter if exists.
258
259     If the user isn't logged in, redirects to settings.LOGIN_URL.
260
261     **Arguments**
262
263     ``template_name``
264         A custom template to use. This is optional; if not specified,
265         this will default to ``im/profile.html``.
266
267     ``extra_context``
268         An dictionary of variables to add to the template context.
269
270     **Template:**
271
272     im/profile.html or ``template_name`` keyword argument.
273
274     **Settings:**
275
276     The view expectes the following settings are defined:
277
278     * LOGIN_URL: login uri
279     """
280     extra_context = extra_context or {}
281     form = ProfileForm(instance=request.user)
282     extra_context['next'] = request.GET.get('next')
283     reset_cookie = False
284     if request.method == 'POST':
285         form = ProfileForm(request.POST, instance=request.user)
286         if form.is_valid():
287             try:
288                 prev_token = request.user.auth_token
289                 user = form.save()
290                 reset_cookie = user.auth_token != prev_token
291                 form = ProfileForm(instance=user)
292                 next = request.POST.get('next')
293                 if next:
294                     return redirect(next)
295                 msg = _('Profile has been updated successfully')
296                 messages.success(request, msg)
297             except ValueError, ve:
298                 messages.success(request, ve)
299     elif request.method == "GET":
300         if not request.user.is_verified:
301             request.user.is_verified = True
302             request.user.save()
303     return render_response(template_name,
304                            reset_cookie=reset_cookie,
305                            profile_form=form,
306                            context_instance=get_context(request,
307                                                         extra_context))
308
309
310 @transaction.commit_manually
311 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
312     """
313     Allows a user to create a local account.
314
315     In case of GET request renders a form for entering the user information.
316     In case of POST handles the signup.
317
318     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
319     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
320     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
321     (see activation_backends);
322
323     Upon successful user creation, if ``next`` url parameter is present the user is redirected there
324     otherwise renders the same page with a success message.
325
326     On unsuccessful creation, renders ``template_name`` with an error message.
327
328     **Arguments**
329
330     ``template_name``
331         A custom template to render. This is optional;
332         if not specified, this will default to ``im/signup.html``.
333
334     ``on_success``
335         A custom template to render in case of success. This is optional;
336         if not specified, this will default to ``im/signup_complete.html``.
337
338     ``extra_context``
339         An dictionary of variables to add to the template context.
340
341     **Template:**
342
343     im/signup.html or ``template_name`` keyword argument.
344     im/signup_complete.html or ``on_success`` keyword argument.
345     """
346     if request.user.is_authenticated():
347         return HttpResponseRedirect(reverse('edit_profile'))
348
349     provider = get_query(request).get('provider', 'local')
350     try:
351         if not backend:
352             backend = get_backend(request)
353         form = backend.get_signup_form(provider)
354     except Exception, e:
355         form = SimpleBackend(request).get_signup_form(provider)
356         messages.error(request, e)
357     if request.method == 'POST':
358         if form.is_valid():
359             user = form.save(commit=False)
360             try:
361                 result = backend.handle_activation(user)
362                 status = messages.SUCCESS
363                 message = result.message
364                 user.save()
365                 if 'additional_email' in form.cleaned_data:
366                     additional_email = form.cleaned_data['additional_email']
367                     if additional_email != user.email:
368                         user.additionalmail_set.create(email=additional_email)
369                         msg = 'Additional email: %s saved for user %s.' % (
370                             additional_email, user.email)
371                         logger.log(LOGGING_LEVEL, msg)
372                 if user and user.is_active:
373                     next = request.POST.get('next', '')
374                     transaction.commit()
375                     return prepare_response(request, user, next=next)
376                 messages.add_message(request, status, message)
377                 transaction.commit()
378                 return render_response(on_success,
379                                        context_instance=get_context(request, extra_context))
380             except SendMailError, e:
381                 message = e.message
382                 messages.error(request, message)
383                 transaction.rollback()
384             except BaseException, e:
385                 message = _('Something went wrong.')
386                 messages.error(request, message)
387                 logger.exception(e)
388                 transaction.rollback()
389     return render_response(template_name,
390                            signup_form=form,
391                            provider=provider,
392                            context_instance=get_context(request, extra_context))
393
394
395 @login_required
396 @signed_terms_required
397 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
398     """
399     Allows a user to send feedback.
400
401     In case of GET request renders a form for providing the feedback information.
402     In case of POST sends an email to support team.
403
404     If the user isn't logged in, redirects to settings.LOGIN_URL.
405
406     **Arguments**
407
408     ``template_name``
409         A custom template to use. This is optional; if not specified,
410         this will default to ``im/feedback.html``.
411
412     ``extra_context``
413         An dictionary of variables to add to the template context.
414
415     **Template:**
416
417     im/signup.html or ``template_name`` keyword argument.
418
419     **Settings:**
420
421     * LOGIN_URL: login uri
422     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
423     """
424     if request.method == 'GET':
425         form = FeedbackForm()
426     if request.method == 'POST':
427         if not request.user:
428             return HttpResponse('Unauthorized', status=401)
429
430         form = FeedbackForm(request.POST)
431         if form.is_valid():
432             msg = form.cleaned_data['feedback_msg']
433             data = form.cleaned_data['feedback_data']
434             try:
435                 send_feedback(msg, data, request.user, email_template_name)
436             except SendMailError, e:
437                 messages.error(request, message)
438             else:
439                 message = _('Feedback successfully sent')
440                 messages.success(request, message)
441     return render_response(template_name,
442                            feedback_form=form,
443                            context_instance=get_context(request, extra_context))
444
445
446 @signed_terms_required
447 def logout(request, template='registration/logged_out.html', extra_context=None):
448     """
449     Wraps `django.contrib.auth.logout` and delete the cookie.
450     """
451     response = HttpResponse()
452     if request.user.is_authenticated():
453         email = request.user.email
454         auth_logout(request)
455         response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
456         msg = 'Cookie deleted for %s' % email
457         logger.log(LOGGING_LEVEL, msg)
458     next = request.GET.get('next')
459     if next:
460         response['Location'] = next
461         response.status_code = 302
462         return response
463     elif LOGOUT_NEXT:
464         response['Location'] = LOGOUT_NEXT
465         response.status_code = 301
466         return response
467     messages.success(request, _('You have successfully logged out.'))
468     context = get_context(request, extra_context)
469     response.write(loader.render_to_string(template, context_instance=context))
470     return response
471
472
473 @transaction.commit_manually
474 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
475     """
476     Activates the user identified by the ``auth`` request parameter, sends a welcome email
477     and renews the user token.
478
479     The view uses commit_manually decorator in order to ensure the user state will be updated
480     only if the email will be send successfully.
481     """
482     token = request.GET.get('auth')
483     next = request.GET.get('next')
484     try:
485         user = AstakosUser.objects.get(auth_token=token)
486     except AstakosUser.DoesNotExist:
487         return HttpResponseBadRequest(_('No such user'))
488
489     if user.is_active:
490         message = _('Account already active.')
491         messages.error(request, message)
492         return index(request)
493
494     try:
495         local_user = AstakosUser.objects.get(
496             ~Q(id=user.id),
497             email=user.email,
498             is_active=True
499         )
500     except AstakosUser.DoesNotExist:
501         try:
502             activate_func(
503                 user,
504                 greeting_email_template_name,
505                 helpdesk_email_template_name,
506                 verify_email=True
507             )
508             response = prepare_response(request, user, next, renew=True)
509             transaction.commit()
510             return response
511         except SendMailError, e:
512             message = e.message
513             messages.error(request, message)
514             transaction.rollback()
515             return index(request)
516         except BaseException, e:
517             message = _('Something went wrong.')
518             messages.error(request, message)
519             logger.exception(e)
520             transaction.rollback()
521             return index(request)
522     else:
523         try:
524             user = switch_account_to_shibboleth(
525                 user,
526                 local_user,
527                 greeting_email_template_name
528             )
529             response = prepare_response(request, user, next, renew=True)
530             transaction.commit()
531             return response
532         except SendMailError, e:
533             message = e.message
534             messages.error(request, message)
535             transaction.rollback()
536             return index(request)
537         except BaseException, e:
538             message = _('Something went wrong.')
539             messages.error(request, message)
540             logger.exception(e)
541             transaction.rollback()
542             return index(request)
543
544
545 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
546     term = None
547     terms = None
548     if not term_id:
549         try:
550             term = ApprovalTerms.objects.order_by('-id')[0]
551         except IndexError:
552             pass
553     else:
554         try:
555             term = ApprovalTerms.objects.get(id=term_id)
556         except ApprovalTerms.DoesNotExist, e:
557             pass
558
559     if not term:
560         return HttpResponseRedirect(reverse('index'))
561     f = open(term.location, 'r')
562     terms = f.read()
563
564     if request.method == 'POST':
565         next = request.POST.get('next')
566         if not next:
567             next = reverse('index')
568         form = SignApprovalTermsForm(request.POST, instance=request.user)
569         if not form.is_valid():
570             return render_response(template_name,
571                                    terms=terms,
572                                    approval_terms_form=form,
573                                    context_instance=get_context(request, extra_context))
574         user = form.save()
575         return HttpResponseRedirect(next)
576     else:
577         form = None
578         if request.user.is_authenticated() and not request.user.signed_terms:
579             form = SignApprovalTermsForm(instance=request.user)
580         return render_response(template_name,
581                                terms=terms,
582                                approval_terms_form=form,
583                                context_instance=get_context(request, extra_context))
584
585
586 @signed_terms_required
587 def change_password(request):
588     return password_change(request,
589                            post_change_redirect=reverse('edit_profile'),
590                            password_change_form=ExtendedPasswordChangeForm)
591
592
593 @signed_terms_required
594 @login_required
595 @transaction.commit_manually
596 def change_email(request, activation_key=None,
597                  email_template_name='registration/email_change_email.txt',
598                  form_template_name='registration/email_change_form.html',
599                  confirm_template_name='registration/email_change_done.html',
600                  extra_context=None):
601     if activation_key:
602         try:
603             user = EmailChange.objects.change_email(activation_key)
604             if request.user.is_authenticated() and request.user == user:
605                 msg = _('Email changed successfully.')
606                 messages.success(request, msg)
607                 auth_logout(request)
608                 response = prepare_response(request, user)
609                 transaction.commit()
610                 return response
611         except ValueError, e:
612             messages.error(request, e)
613         return render_response(confirm_template_name,
614                                modified_user=user if 'user' in locals(
615                                ) else None,
616                                context_instance=get_context(request,
617                                                             extra_context))
618
619     if not request.user.is_authenticated():
620         path = quote(request.get_full_path())
621         url = request.build_absolute_uri(reverse('index'))
622         return HttpResponseRedirect(url + '?next=' + path)
623     form = EmailChangeForm(request.POST or None)
624     if request.method == 'POST' and form.is_valid():
625         try:
626             ec = form.save(email_template_name, request)
627         except SendMailError, e:
628             msg = e
629             messages.error(request, msg)
630             transaction.rollback()
631         except IntegrityError, e:
632             msg = _('There is already a pending change email request.')
633             messages.error(request, msg)
634         else:
635             msg = _('Change email request has been registered succefully.\
636                     You are going to receive a verification email in the new address.')
637             messages.success(request, msg)
638             transaction.commit()
639     return render_response(form_template_name,
640                            form=form,
641                            context_instance=get_context(request,
642                                                         extra_context))
643
644
645 @signed_terms_required
646 @login_required
647 def group_add(request, kind_name='default'):
648     try:
649         kind = GroupKind.objects.get(name=kind_name)
650     except:
651         return HttpResponseBadRequest(_('No such group kind'))
652
653     template_loader = loader
654     post_save_redirect = '/im/group/%(id)s/'
655     context_processors = None
656     model, form_class = get_model_and_form_class(
657         model=None,
658         form_class=AstakosGroupCreationForm
659     )
660     resources = dict(
661         (str(r.id), r) for r in Resource.objects.select_related().all())
662     policies = []
663     if request.method == 'POST':
664         form = form_class(request.POST, request.FILES, resources=resources)
665         if form.is_valid():
666             new_object = form.save()
667
668             # save owner
669             new_object.owners = [request.user]
670
671             # save quota policies
672             for (rid, uplimit) in form.resources():
673                 try:
674                     r = resources[rid]
675                 except KeyError, e:
676                     logger.exception(e)
677                     # TODO Should I stay or should I go???
678                     continue
679                 else:
680                     new_object.astakosgroupquota_set.create(
681                         resource=r,
682                         uplimit=uplimit
683                     )
684                 policies.append('%s %d' % (r, uplimit))
685             msg = _("The %(verbose_name)s was created successfully.") %\
686                 {"verbose_name": model._meta.verbose_name}
687             messages.success(request, msg, fail_silently=True)
688
689             # send notification
690             try:
691                 send_admin_notification(
692                     template_name='im/group_creation_notification.txt',
693                     dictionary={
694                         'group': new_object,
695                         'owner': request.user,
696                         'policies': policies,
697                     },
698                     subject='%s alpha2 testing group creation notification' % SITENAME
699                 )
700             except SendNotificationError, e:
701                 messages.error(request, e, fail_silently=True)
702             return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
703     else:
704         now = datetime.now()
705         data = {
706             'kind': kind
707         }
708         form = form_class(data, resources=resources)
709
710     # Create the template, context, response
711     template_name = "%s/%s_form.html" % (
712         model._meta.app_label,
713         model._meta.object_name.lower()
714     )
715     t = template_loader.get_template(template_name)
716     c = RequestContext(request, {
717         'form': form,
718         'kind': kind,
719     }, context_processors)
720     return HttpResponse(t.render(c))
721
722
723 @signed_terms_required
724 @login_required
725 def group_list(request):
726     none = request.user.astakos_groups.none()
727     q = AstakosGroup.objects.raw("""
728         SELECT auth_group.id,
729         %s AS groupname,
730         im_groupkind.name AS kindname,
731         im_astakosgroup.*,
732         owner.email AS groupowner,
733         (SELECT COUNT(*) FROM im_membership
734             WHERE group_id = im_astakosgroup.group_ptr_id
735             AND date_joined IS NOT NULL) AS approved_members_num,
736         (SELECT CASE WHEN(
737                     SELECT date_joined FROM im_membership
738                     WHERE group_id = im_astakosgroup.group_ptr_id
739                     AND person_id = %s) IS NULL
740                     THEN 0 ELSE 1 END) AS membership_status
741         FROM im_astakosgroup
742         INNER JOIN im_membership ON (
743             im_astakosgroup.group_ptr_id = im_membership.group_id)
744         INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
745         INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
746         LEFT JOIN im_astakosuser_owner ON (
747             im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
748         LEFT JOIN auth_user as owner ON (
749             im_astakosuser_owner.astakosuser_id = owner.id)
750         WHERE im_membership.person_id = %s
751         """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
752     d = defaultdict(list)
753     for g in q:
754         if request.user.email == g.groupowner:
755             d['own'].append(g)
756         else:
757             d['other'].append(g)
758     
759     for k, l in d.iteritems():
760         page = request.GET.get('%s_page' % k, 1)
761         sorting = globals()['%s_sorting' % k] = request.GET.get('%s_sorting' % k)
762         if sorting:
763             sort_form = AstakosGroupSortForm({'sort_by': sorting})
764             if sort_form.is_valid():
765                 sort_field = q._model_fields.get(sorting)
766                 default = _get_default(sort_field)
767                 l.sort(key=lambda i: getattr(i, sorting) if getattr(i, sorting) else default)
768                 globals()['%s_sorting' % k] = sorting
769         paginator = Paginator(l, PAGINATE_BY)
770         
771         try:
772             page_number = int(page)
773         except ValueError:
774             if page == 'last':
775                 page_number = paginator.num_pages
776             else:
777                 # Page is not 'last', nor can it be converted to an int.
778                 raise Http404
779         try:
780             page_obj = globals()['%s_page_obj' % k] = paginator.page(page_number)
781         except InvalidPage:
782             raise Http404
783     return object_list(request, queryset=none,
784                        extra_context={'is_search':False,
785                                       'mine': own_page_obj,
786                                       'other': other_page_obj,
787                                       'own_sorting': own_sorting,
788                                       'other_sorting': other_sorting
789                                       })
790
791
792 @signed_terms_required
793 @login_required
794 def group_detail(request, group_id):
795     q = AstakosGroup.objects.select_related().filter(pk=group_id)
796     q = q.extra(select={
797         'is_member': """SELECT CASE WHEN EXISTS(
798                             SELECT id FROM im_membership
799                             WHERE group_id = im_astakosgroup.group_ptr_id
800                             AND person_id = %s)
801                         THEN 1 ELSE 0 END""" % request.user.id,
802         'is_owner': """SELECT CASE WHEN EXISTS(
803                         SELECT id FROM im_astakosuser_owner
804                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
805                         AND astakosuser_id = %s)
806                         THEN 1 ELSE 0 END""" % request.user.id,
807         'kindname': """SELECT name FROM im_groupkind
808                        WHERE id = im_astakosgroup.kind_id"""})
809     
810     model = q.model
811     context_processors = None
812     mimetype = None
813     try:
814         obj = q.get()
815     except AstakosGroup.DoesNotExist:
816         raise Http404("No %s found matching the query" % (
817             model._meta.verbose_name))
818     
819     update_form = AstakosGroupUpdateForm(instance=obj)
820     addmembers_form = AddGroupMembersForm()
821     if request.method == 'POST':
822         update_data = {}
823         addmembers_data = {}
824         for k,v in request.POST.iteritems():
825             if k in update_form.fields:
826                 update_data[k] = v
827             if k in addmembers_form.fields:
828                 addmembers_data[k] = v
829         update_data = update_data or None
830         addmembers_data = addmembers_data or None
831         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
832         addmembers_form = AddGroupMembersForm(addmembers_data)
833         if update_form.is_valid():
834             update_form.save()
835         if addmembers_form.is_valid():
836             map(obj.approve_member, addmembers_form.valid_users)
837             addmembers_form = AddGroupMembersForm()
838     
839     template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
840     t = template_loader.get_template(template_name)
841     c = RequestContext(request, {
842         'object': obj,
843     }, context_processors)
844     extra_context = {'update_form': update_form,
845                      'addmembers_form': addmembers_form,
846                      'page': request.GET.get('page', 1),
847                      'sorting': request.GET.get('sorting')}
848     for key, value in extra_context.items():
849         if callable(value):
850             c[key] = value()
851         else:
852             c[key] = value
853     response = HttpResponse(t.render(c), mimetype=mimetype)
854     populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
855     return response
856
857
858 @signed_terms_required
859 @login_required
860 def group_search(request, extra_context=None, **kwargs):
861     q = request.GET.get('q')
862     sorting = request.GET.get('sorting')
863     if request.method == 'GET':
864         form = AstakosGroupSearchForm({'q': q} if q else None)
865     else:
866         form = AstakosGroupSearchForm(get_query(request))
867         if form.is_valid():
868             q = form.cleaned_data['q'].strip()
869     if q:
870         queryset = AstakosGroup.objects.select_related()
871         queryset = queryset.filter(name__contains=q)
872         queryset = queryset.filter(approval_date__isnull=False)
873         queryset = queryset.extra(select={
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,
884                 'is_member': """
885                     SELECT CASE WHEN EXISTS(
886                     SELECT date_joined FROM im_membership
887                     WHERE group_id = im_astakosgroup.group_ptr_id
888                     AND person_id = %s)
889                     THEN 1 ELSE 0 END""" % request.user.id,
890                 'is_owner': """
891                     SELECT CASE WHEN EXISTS(
892                     SELECT id FROM im_astakosuser_owner
893                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
894                     AND astakosuser_id = %s)
895                     THEN 1 ELSE 0 END""" % request.user.id})
896         if sorting:
897             # TODO check sorting value
898             queryset = queryset.order_by(sorting)
899     else:
900         queryset = AstakosGroup.objects.none()
901     return object_list(
902         request,
903         queryset,
904         paginate_by=PAGINATE_BY,
905         page=request.GET.get('page') or 1,
906         template_name='im/astakosgroup_list.html',
907         extra_context=dict(form=form,
908                            is_search=True,
909                            q=q,
910                            sorting=sorting))
911
912 @signed_terms_required
913 @login_required
914 def group_all(request, extra_context=None, **kwargs):
915     q = AstakosGroup.objects.select_related()
916     q = q.filter(approval_date__isnull=False)
917     q = q.extra(select={
918                 'groupname': DB_REPLACE_GROUP_SCHEME,
919                 'kindname': "im_groupkind.name",
920                 'approved_members_num': """
921                     SELECT COUNT(*) FROM im_membership
922                     WHERE group_id = im_astakosgroup.group_ptr_id
923                     AND date_joined IS NOT NULL""",
924                 'membership_approval_date': """
925                     SELECT date_joined FROM im_membership
926                     WHERE group_id = im_astakosgroup.group_ptr_id
927                     AND person_id = %s""" % request.user.id,
928                 'is_member': """
929                     SELECT CASE WHEN EXISTS(
930                     SELECT date_joined FROM im_membership
931                     WHERE group_id = im_astakosgroup.group_ptr_id
932                     AND person_id = %s)
933                     THEN 1 ELSE 0 END""" % request.user.id})
934     sorting = request.GET.get('sorting')
935     if sorting:
936         # TODO check sorting value
937         q = q.order_by(sorting)
938     return object_list(
939                 request,
940                 q,
941                 paginate_by=PAGINATE_BY,
942                 page=request.GET.get('page') or 1,
943                 template_name='im/astakosgroup_list.html',
944                 extra_context=dict(form=AstakosGroupSearchForm(),
945                                    is_search=True,
946                                    sorting=sorting))
947
948
949 @signed_terms_required
950 @login_required
951 def group_join(request, group_id):
952     m = Membership(group_id=group_id,
953                    person=request.user,
954                    date_requested=datetime.now())
955     try:
956         m.save()
957         post_save_redirect = reverse(
958             'group_detail',
959             kwargs=dict(group_id=group_id))
960         return HttpResponseRedirect(post_save_redirect)
961     except IntegrityError, e:
962         logger.exception(e)
963         msg = _('Failed to join group.')
964         messages.error(request, msg)
965         return group_search(request)
966
967
968 @signed_terms_required
969 @login_required
970 def group_leave(request, group_id):
971     try:
972         m = Membership.objects.select_related().get(
973             group__id=group_id,
974             person=request.user)
975     except Membership.DoesNotExist:
976         return HttpResponseBadRequest(_('Invalid membership.'))
977     if request.user in m.group.owner.all():
978         return HttpResponseForbidden(_('Owner can not leave the group.'))
979     return delete_object(
980         request,
981         model=Membership,
982         object_id=m.id,
983         template_name='im/astakosgroup_list.html',
984         post_delete_redirect=reverse(
985             'group_detail',
986             kwargs=dict(group_id=group_id)))
987
988
989 def handle_membership(func):
990     @wraps(func)
991     def wrapper(request, group_id, user_id):
992         try:
993             m = Membership.objects.select_related().get(
994                 group__id=group_id,
995                 person__id=user_id)
996         except Membership.DoesNotExist:
997             return HttpResponseBadRequest(_('Invalid membership.'))
998         else:
999             if request.user not in m.group.owner.all():
1000                 return HttpResponseForbidden(_('User is not a group owner.'))
1001             func(request, m)
1002             return render_response(
1003                 template='im/astakosgroup_detail.html',
1004                 context_instance=get_context(request),
1005                 object=m.group,
1006                 quota=m.group.quota)
1007     return wrapper
1008
1009
1010 @signed_terms_required
1011 @login_required
1012 @handle_membership
1013 def approve_member(request, membership):
1014     try:
1015         membership.approve()
1016         realname = membership.person.realname
1017         msg = _('%s has been successfully joined the group.' % realname)
1018         messages.success(request, msg)
1019     except BaseException, e:
1020         logger.exception(e)
1021         msg = _('Something went wrong during %s\'s approval.' % realname)
1022         messages.error(request, msg)
1023
1024
1025 @signed_terms_required
1026 @login_required
1027 @handle_membership
1028 def disapprove_member(request, membership):
1029     try:
1030         membership.disapprove()
1031         realname = membership.person.realname
1032         msg = _('%s has been successfully removed from the group.' % realname)
1033         messages.success(request, msg)
1034     except BaseException, e:
1035         logger.exception(e)
1036         msg = _('Something went wrong during %s\'s disapproval.' % realname)
1037         messages.error(request, msg)
1038
1039
1040 @signed_terms_required
1041 @login_required
1042 def resource_list(request):
1043     return render_response(
1044         template='im/astakosuserquota_list.html',
1045         context_instance=get_context(request),
1046         quota=request.user.quota)
1047
1048
1049 def group_create_list(request):
1050     return render_response(
1051         template='im/astakosgroup_create_list.html',
1052         context_instance=get_context(request),)
1053
1054
1055 @signed_terms_required
1056 @login_required
1057 def billing(request):
1058     
1059     today = datetime.today()
1060     month_last_day= calendar.monthrange(today.year, today.month)[1]
1061     
1062     start = request.POST.get('datefrom', None)
1063     if start:
1064         today = datetime.fromtimestamp(int(start))
1065         month_last_day= calendar.monthrange(today.year, today.month)[1]
1066     
1067     start = datetime(today.year, today.month, 1).strftime("%s")
1068     end = datetime(today.year, today.month, month_last_day).strftime("%s")
1069     r = request_billing.apply(args=('pgerakios@grnet.gr',
1070                                     int(start) * 1000,
1071                                     int(end) * 1000))
1072     data = {}
1073     
1074     try:
1075         status, data = r.result
1076         data=_clear_billing_data(data)
1077         if status != 200:
1078             messages.error(request, _('Service response status: %d' % status))
1079     except:
1080         messages.error(request, r.result)
1081     
1082     print type(start)
1083     
1084     return render_response(
1085         template='im/billing.html',
1086         context_instance=get_context(request),
1087         data=data,
1088         zerodate=datetime(month=1,year=1970, day=1),
1089         today=today,
1090         start=int(start),
1091         month_last_day=month_last_day)  
1092     
1093 def _clear_billing_data(data):
1094     
1095     # remove addcredits entries
1096     def isnotcredit(e):
1097         return e['serviceName'] != "addcredits"
1098     
1099     
1100     
1101     # separate services    
1102     def servicefilter(service_name):
1103         service = service_name
1104         def fltr(e):
1105             return e['serviceName'] == service
1106         return fltr
1107         
1108     
1109     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1110     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1111     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1112     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1113         
1114     return data
1115
1116
1117 def _get_default(field):
1118     if isinstance(field, DateTimeField):
1119         return datetime.utcfromtimestamp(0)
1120     elif isinstance(field, int):
1121         return 0
1122     return ''