Minor changes in group list
[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.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 ObjectDoesNotExist:
816         raise Http404("No %s found matching the query" % (model._meta.verbose_name))
817     
818     update_form = AstakosGroupUpdateForm(instance=obj)
819     addmembers_form = AddGroupMembersForm()
820     if request.method == 'POST':
821         update_data = {}
822         addmembers_data = {}
823         for k,v in request.POST.iteritems():
824             if k in update_form.fields:
825                 update_data[k] = v
826             if k in addmembers_form.fields:
827                 addmembers_data[k] = v
828         update_data = update_data or None
829         addmembers_data = addmembers_data or None
830         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
831         addmembers_form = AddGroupMembersForm(addmembers_data)
832         if update_form.is_valid():
833             update_form.save()
834         if addmembers_form.is_valid():
835             map(obj.approve_member, addmembers_form.valid_users)
836             addmembers_form = AddGroupMembersForm()
837     
838     template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
839     t = template_loader.get_template(template_name)
840     c = RequestContext(request, {
841         'object': obj,
842     }, context_processors)
843     extra_context = {'update_form': update_form,
844                      'addmembers_form': addmembers_form,
845                      'page': request.GET.get('page', 1),
846                      'sorting': request.GET.get('sorting')}
847     for key, value in extra_context.items():
848         if callable(value):
849             c[key] = value()
850         else:
851             c[key] = value
852     response = HttpResponse(t.render(c), mimetype=mimetype)
853     populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
854     return response
855
856
857 @signed_terms_required
858 @login_required
859 def group_search(request, extra_context=None, **kwargs):
860     q = request.GET.get('q')
861     sorting = request.GET.get('sorting')
862     if request.method == 'GET':
863         form = AstakosGroupSearchForm({'q': q} if q else None)
864     else:
865         form = AstakosGroupSearchForm(get_query(request))
866         if form.is_valid():
867             q = form.cleaned_data['q'].strip()
868     if q:
869         queryset = AstakosGroup.objects.select_related()
870         queryset = queryset.filter(name__contains=q)
871         queryset = queryset.filter(approval_date__isnull=False)
872         queryset = queryset.extra(select={
873                 'groupname': DB_REPLACE_GROUP_SCHEME,
874                 'kindname': "im_groupkind.name",
875                 'approved_members_num': """
876                     SELECT COUNT(*) FROM im_membership
877                     WHERE group_id = im_astakosgroup.group_ptr_id
878                     AND date_joined IS NOT NULL""",
879                 'membership_approval_date': """
880                     SELECT date_joined FROM im_membership
881                     WHERE group_id = im_astakosgroup.group_ptr_id
882                     AND person_id = %s""" % request.user.id,
883                 'is_member': """
884                     SELECT CASE WHEN EXISTS(
885                     SELECT date_joined FROM im_membership
886                     WHERE group_id = im_astakosgroup.group_ptr_id
887                     AND person_id = %s)
888                     THEN 1 ELSE 0 END""" % request.user.id})
889         if sorting:
890             # TODO check sorting value
891             queryset = queryset.order_by(sorting)
892     else:
893         queryset = AstakosGroup.objects.none()
894     return object_list(
895         request,
896         queryset,
897         paginate_by=PAGINATE_BY,
898         page=request.GET.get('page') or 1,
899         template_name='im/astakosgroup_list.html',
900         extra_context=dict(form=form,
901                            is_search=True,
902                            q=q,
903                            sorting=sorting))
904
905 @signed_terms_required
906 @login_required
907 def group_all(request, extra_context=None, **kwargs):
908     q = AstakosGroup.objects.select_related()
909     q = q.filter(approval_date__isnull=False)
910     q = q.extra(select={
911                 'groupname': DB_REPLACE_GROUP_SCHEME,
912                 'kindname': "im_groupkind.name",
913                 'approved_members_num': """
914                     SELECT COUNT(*) FROM im_membership
915                     WHERE group_id = im_astakosgroup.group_ptr_id
916                     AND date_joined IS NOT NULL""",
917                 'membership_approval_date': """
918                     SELECT date_joined FROM im_membership
919                     WHERE group_id = im_astakosgroup.group_ptr_id
920                     AND person_id = %s""" % request.user.id,
921                 'is_member': """
922                     SELECT CASE WHEN EXISTS(
923                     SELECT date_joined FROM im_membership
924                     WHERE group_id = im_astakosgroup.group_ptr_id
925                     AND person_id = %s)
926                     THEN 1 ELSE 0 END""" % request.user.id})
927     sorting = request.GET.get('sorting')
928     if sorting:
929         # TODO check sorting value
930         q = q.order_by(sorting)
931     return object_list(
932                 request,
933                 q,
934                 paginate_by=PAGINATE_BY,
935                 page=request.GET.get('page') or 1,
936                 template_name='im/astakosgroup_list.html',
937                 extra_context=dict(form=AstakosGroupSearchForm(),
938                                    is_search=True,
939                                    sorting=sorting))
940
941
942 @signed_terms_required
943 @login_required
944 def group_join(request, group_id):
945     m = Membership(group_id=group_id,
946                    person=request.user,
947                    date_requested=datetime.now())
948     try:
949         m.save()
950         post_save_redirect = reverse(
951             'group_detail',
952             kwargs=dict(group_id=group_id))
953         return HttpResponseRedirect(post_save_redirect)
954     except IntegrityError, e:
955         logger.exception(e)
956         msg = _('Failed to join group.')
957         messages.error(request, msg)
958         return group_search(request)
959
960
961 @signed_terms_required
962 @login_required
963 def group_leave(request, group_id):
964     try:
965         m = Membership.objects.select_related().get(
966             group__id=group_id,
967             person=request.user
968         )
969     except Membership.DoesNotExist:
970         return HttpResponseBadRequest(_('Invalid membership.'))
971     if request.user in m.group.owner.all():
972         return HttpResponseForbidden(_('Owner can not leave the group.'))
973     return delete_object(
974         request,
975         model=Membership,
976         object_id=m.id,
977         template_name='im/astakosgroup_list.html',
978         post_delete_redirect=reverse(
979             'group_detail',
980             kwargs=dict(group_id=group_id)
981         )
982     )
983
984
985 def handle_membership(func):
986     @wraps(func)
987     def wrapper(request, group_id, user_id):
988         try:
989             m = Membership.objects.select_related().get(
990                 group__id=group_id,
991                 person__id=user_id
992             )
993         except Membership.DoesNotExist:
994             return HttpResponseBadRequest(_('Invalid membership.'))
995         else:
996             if request.user not in m.group.owner.all():
997                 return HttpResponseForbidden(_('User is not a group owner.'))
998             func(request, m)
999             return render_response(
1000                 template='im/astakosgroup_detail.html',
1001                 context_instance=get_context(request),
1002                 object=m.group,
1003                 quota=m.group.quota
1004             )
1005     return wrapper
1006
1007
1008 @signed_terms_required
1009 @login_required
1010 @handle_membership
1011 def approve_member(request, membership):
1012     try:
1013         membership.approve()
1014         realname = membership.person.realname
1015         msg = _('%s has been successfully joined the group.' % realname)
1016         messages.success(request, msg)
1017     except BaseException, e:
1018         logger.exception(e)
1019         msg = _('Something went wrong during %s\'s approval.' % realname)
1020         messages.error(request, msg)
1021
1022
1023 @signed_terms_required
1024 @login_required
1025 @handle_membership
1026 def disapprove_member(request, membership):
1027     try:
1028         membership.disapprove()
1029         realname = membership.person.realname
1030         msg = _('%s has been successfully removed from the group.' % realname)
1031         messages.success(request, msg)
1032     except BaseException, e:
1033         logger.exception(e)
1034         msg = _('Something went wrong during %s\'s disapproval.' % realname)
1035         messages.error(request, msg)
1036
1037
1038 @signed_terms_required
1039 @login_required
1040 def resource_list(request):
1041     return render_response(
1042         template='im/astakosuserquota_list.html',
1043         context_instance=get_context(request),
1044         quota=request.user.quota
1045     )
1046
1047
1048 def group_create_list(request):
1049     return render_response(
1050         template='im/astakosgroup_create_list.html',
1051         context_instance=get_context(request),
1052     )
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 ''