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