Progress VII
[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 socket
36
37 from smtplib import SMTPException
38 from urllib import quote
39 from functools import wraps
40
41 from django.contrib import messages
42 from django.contrib.auth.decorators import login_required
43 from django.contrib.auth.views import password_change
44 from django.core.exceptions import ValidationError
45 from django.core.mail import send_mail
46 from django.core.urlresolvers import reverse
47 from django.db import transaction
48 from django.db.models import Q
49 from django.db.utils import IntegrityError
50 from django.forms.fields import URLField
51 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
52     HttpResponseRedirect, HttpResponseBadRequest
53 from django.shortcuts import redirect
54 from django.template.loader import render_to_string
55 from django.utils.http import urlencode
56 from django.utils.translation import ugettext as _
57 from django.views.generic.create_update import *
58 from django.views.generic.list_detail import *
59
60 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms, AstakosGroup, Resource
61 from astakos.im.activation_backends import get_backend, SimpleBackend
62 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
63 from astakos.im.forms import *
64 from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
65     invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
66 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
67
68 logger = logging.getLogger(__name__)
69
70 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
71     """
72     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
73     keyword argument and returns an ``django.http.HttpResponse`` with the
74     specified ``status``.
75     """
76     if tab is None:
77         tab = template.partition('_')[0].partition('.html')[0]
78     kwargs.setdefault('tab', tab)
79     html = render_to_string(template, kwargs, context_instance=context_instance)
80     response = HttpResponse(html, status=status)
81     if reset_cookie:
82         set_cookie(response, context_instance['request'].user)
83     return response
84
85
86 def requires_anonymous(func):
87     """
88     Decorator checkes whether the request.user is not Anonymous and in that case
89     redirects to `logout`.
90     """
91     @wraps(func)
92     def wrapper(request, *args):
93         if not request.user.is_anonymous():
94             next = urlencode({'next': request.build_absolute_uri()})
95             logout_uri = reverse(logout) + '?' + next
96             return HttpResponseRedirect(logout_uri)
97         return func(request, *args)
98     return wrapper
99
100 def signed_terms_required(func):
101     """
102     Decorator checkes whether the request.user is Anonymous and in that case
103     redirects to `logout`.
104     """
105     @wraps(func)
106     def wrapper(request, *args, **kwargs):
107         if request.user.is_authenticated() and not request.user.signed_terms():
108             params = urlencode({'next': request.build_absolute_uri(),
109                               'show_form':''})
110             terms_uri = reverse('latest_terms') + '?' + params
111             return HttpResponseRedirect(terms_uri)
112         return func(request, *args, **kwargs)
113     return wrapper
114
115 @signed_terms_required
116 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
117     """
118     If there is logged on user renders the profile page otherwise renders login page.
119
120     **Arguments**
121
122     ``login_template_name``
123         A custom login template to use. This is optional; if not specified,
124         this will default to ``im/login.html``.
125
126     ``profile_template_name``
127         A custom profile template to use. This is optional; if not specified,
128         this will default to ``im/profile.html``.
129
130     ``extra_context``
131         An dictionary of variables to add to the template context.
132
133     **Template:**
134
135     im/profile.html or im/login.html or ``template_name`` keyword argument.
136
137     """
138     template_name = login_template_name
139     if request.user.is_authenticated():
140         return HttpResponseRedirect(reverse('edit_profile'))
141     return render_response(template_name,
142                            login_form = LoginForm(request=request),
143                            context_instance = get_context(request, extra_context))
144
145 @login_required
146 @signed_terms_required
147 @transaction.commit_manually
148 def invite(request, template_name='im/invitations.html', extra_context={}):
149     """
150     Allows a user to invite somebody else.
151
152     In case of GET request renders a form for providing the invitee information.
153     In case of POST checks whether the user has not run out of invitations and then
154     sends an invitation email to singup to the service.
155
156     The view uses commit_manually decorator in order to ensure the number of the
157     user invitations is going to be updated only if the email has been successfully sent.
158
159     If the user isn't logged in, redirects to settings.LOGIN_URL.
160
161     **Arguments**
162
163     ``template_name``
164         A custom template to use. This is optional; if not specified,
165         this will default to ``im/invitations.html``.
166
167     ``extra_context``
168         An dictionary of variables to add to the template context.
169
170     **Template:**
171
172     im/invitations.html or ``template_name`` keyword argument.
173
174     **Settings:**
175
176     The view expectes the following settings are defined:
177
178     * LOGIN_URL: login uri
179     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
180     * ASTAKOS_DEFAULT_FROM_EMAIL: from email
181     """
182     status = None
183     message = None
184     form = InvitationForm()
185     
186     inviter = request.user
187     if request.method == 'POST':
188         form = InvitationForm(request.POST)
189         if inviter.invitations > 0:
190             if form.is_valid():
191                 try:
192                     invitation = form.save()
193                     invite_func(invitation, inviter)
194                     message = _('Invitation sent to %s' % invitation.username)
195                     messages.success(request, message)
196                 except SendMailError, e:
197                     message = e.message
198                     messages.error(request, message)
199                     transaction.rollback()
200                 except BaseException, e:
201                     message = _('Something went wrong.')
202                     messages.error(request, message)
203                     logger.exception(e)
204                     transaction.rollback()
205                 else:
206                     transaction.commit()
207         else:
208             message = _('No invitations left')
209             messages.error(request, message)
210
211     sent = [{'email': inv.username,
212              'realname': inv.realname,
213              'is_consumed': inv.is_consumed}
214              for inv in request.user.invitations_sent.all()]
215     kwargs = {'inviter': inviter,
216               'sent':sent}
217     context = get_context(request, extra_context, **kwargs)
218     return render_response(template_name,
219                            invitation_form = form,
220                            context_instance = context)
221
222 @login_required
223 @signed_terms_required
224 def edit_profile(request, template_name='im/profile.html', extra_context={}):
225     """
226     Allows a user to edit his/her profile.
227
228     In case of GET request renders a form for displaying the user information.
229     In case of POST updates the user informantion and redirects to ``next``
230     url parameter if exists.
231
232     If the user isn't logged in, redirects to settings.LOGIN_URL.
233
234     **Arguments**
235
236     ``template_name``
237         A custom template to use. This is optional; if not specified,
238         this will default to ``im/profile.html``.
239
240     ``extra_context``
241         An dictionary of variables to add to the template context.
242
243     **Template:**
244
245     im/profile.html or ``template_name`` keyword argument.
246
247     **Settings:**
248
249     The view expectes the following settings are defined:
250
251     * LOGIN_URL: login uri
252     """
253     form = ProfileForm(instance=request.user)
254     extra_context['next'] = request.GET.get('next')
255     reset_cookie = False
256     if request.method == 'POST':
257         form = ProfileForm(request.POST, instance=request.user)
258         if form.is_valid():
259             try:
260                 prev_token = request.user.auth_token
261                 user = form.save()
262                 reset_cookie = user.auth_token != prev_token
263                 form = ProfileForm(instance=user)
264                 next = request.POST.get('next')
265                 if next:
266                     return redirect(next)
267                 msg = _('Profile has been updated successfully')
268                 messages.success(request, msg)
269             except ValueError, ve:
270                 messages.success(request, ve)
271     elif request.method == "GET":
272         if not request.user.is_verified:
273             request.user.is_verified = True
274             request.user.save()
275     return render_response(template_name,
276                            reset_cookie = reset_cookie,
277                            profile_form = form,
278                            context_instance = get_context(request,
279                                                           extra_context))
280
281 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
282     """
283     Allows a user to create a local account.
284
285     In case of GET request renders a form for entering the user information.
286     In case of POST handles the signup.
287
288     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
289     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
290     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
291     (see activation_backends);
292     
293     Upon successful user creation, if ``next`` url parameter is present the user is redirected there
294     otherwise renders the same page with a success message.
295     
296     On unsuccessful creation, renders ``template_name`` with an error message.
297     
298     **Arguments**
299     
300     ``template_name``
301         A custom template to render. This is optional;
302         if not specified, this will default to ``im/signup.html``.
303
304     ``on_success``
305         A custom template to render in case of success. This is optional;
306         if not specified, this will default to ``im/signup_complete.html``.
307
308     ``extra_context``
309         An dictionary of variables to add to the template context.
310
311     **Template:**
312     
313     im/signup.html or ``template_name`` keyword argument.
314     im/signup_complete.html or ``on_success`` keyword argument. 
315     """
316     if request.user.is_authenticated():
317         return HttpResponseRedirect(reverse('edit_profile'))
318     
319     provider = get_query(request).get('provider', 'local')
320     try:
321         if not backend:
322             backend = get_backend(request)
323         form = backend.get_signup_form(provider)
324     except Exception, e:
325         form = SimpleBackend(request).get_signup_form(provider)
326         messages.error(request, e)
327     if request.method == 'POST':
328         if form.is_valid():
329             user = form.save(commit=False)
330             try:
331                 result = backend.handle_activation(user)
332                 status = messages.SUCCESS
333                 message = result.message
334                 user.save()
335                 if 'additional_email' in form.cleaned_data:
336                     additional_email = form.cleaned_data['additional_email']
337                     if additional_email != user.email:
338                         user.additionalmail_set.create(email=additional_email)
339                         msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
340                         logger._log(LOGGING_LEVEL, msg, [])
341                 if user and user.is_active:
342                     next = request.POST.get('next', '')
343                     return prepare_response(request, user, next=next)
344                 messages.add_message(request, status, message)
345                 return render_response(on_success,
346                                        context_instance=get_context(request, extra_context))
347             except SendMailError, e:
348                 message = e.message
349                 messages.error(request, message)
350             except BaseException, e:
351                 message = _('Something went wrong.')
352                 messages.error(request, message)
353                 logger.exception(e)
354     return render_response(template_name,
355                            signup_form = form,
356                            provider = provider,
357                            context_instance=get_context(request, extra_context))
358
359 @login_required
360 @signed_terms_required
361 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
362     """
363     Allows a user to send feedback.
364
365     In case of GET request renders a form for providing the feedback information.
366     In case of POST sends an email to support team.
367
368     If the user isn't logged in, redirects to settings.LOGIN_URL.
369
370     **Arguments**
371
372     ``template_name``
373         A custom template to use. This is optional; if not specified,
374         this will default to ``im/feedback.html``.
375
376     ``extra_context``
377         An dictionary of variables to add to the template context.
378
379     **Template:**
380
381     im/signup.html or ``template_name`` keyword argument.
382
383     **Settings:**
384
385     * LOGIN_URL: login uri
386     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
387     """
388     if request.method == 'GET':
389         form = FeedbackForm()
390     if request.method == 'POST':
391         if not request.user:
392             return HttpResponse('Unauthorized', status=401)
393
394         form = FeedbackForm(request.POST)
395         if form.is_valid():
396             msg = form.cleaned_data['feedback_msg']
397             data = form.cleaned_data['feedback_data']
398             try:
399                 send_feedback(msg, data, request.user, email_template_name)
400             except SendMailError, e:
401                 status = messages.ERROR
402                 messages.error(request, message)
403             else:
404                 message = _('Feedback successfully sent')
405                 messages.succeess(request, message)
406     return render_response(template_name,
407                            feedback_form = form,
408                            context_instance = get_context(request, extra_context))
409
410 @signed_terms_required
411 def logout(request, template='registration/logged_out.html', extra_context={}):
412     """
413     Wraps `django.contrib.auth.logout` and delete the cookie.
414     """
415     response = HttpResponse()
416     if request.user.is_authenticated():
417         email = request.user.email
418         auth_logout(request)
419         response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
420         msg = 'Cookie deleted for %s' % email
421         logger._log(LOGGING_LEVEL, msg, [])
422     next = request.GET.get('next')
423     if next:
424         response['Location'] = next
425         response.status_code = 302
426         return response
427     elif LOGOUT_NEXT:
428         response['Location'] = LOGOUT_NEXT
429         response.status_code = 301
430         return response
431     messages.success(request, _('You have successfully logged out.'))
432     context = get_context(request, extra_context)
433     response.write(render_to_string(template, context_instance=context))
434     return response
435
436 @transaction.commit_manually
437 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
438     """
439     Activates the user identified by the ``auth`` request parameter, sends a welcome email
440     and renews the user token.
441
442     The view uses commit_manually decorator in order to ensure the user state will be updated
443     only if the email will be send successfully.
444     """
445     token = request.GET.get('auth')
446     next = request.GET.get('next')
447     try:
448         user = AstakosUser.objects.get(auth_token=token)
449     except AstakosUser.DoesNotExist:
450         return HttpResponseBadRequest(_('No such user'))
451     
452     if user.is_active:
453         message = _('Account already active.')
454         messages.error(request, message)
455         return index(request)
456         
457     try:
458         local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
459     except AstakosUser.DoesNotExist:
460         try:
461             activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
462             response = prepare_response(request, user, next, renew=True)
463             transaction.commit()
464             return response
465         except SendMailError, e:
466             message = e.message
467             messages.error(request, message)
468             transaction.rollback()
469             return index(request)
470         except BaseException, e:
471             message = _('Something went wrong.')
472             messages.error(request, message)
473             logger.exception(e)
474             transaction.rollback()
475             return index(request)
476     else:
477         try:
478             user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
479             response = prepare_response(request, user, next, renew=True)
480             transaction.commit()
481             return response
482         except SendMailError, e:
483             message = e.message
484             messages.error(request, message)
485             transaction.rollback()
486             return index(request)
487         except BaseException, e:
488             message = _('Something went wrong.')
489             messages.error(request, message)
490             logger.exception(e)
491             transaction.rollback()
492             return index(request)
493
494 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
495     term = None
496     terms = None
497     if not term_id:
498         try:
499             term = ApprovalTerms.objects.order_by('-id')[0]
500         except IndexError:
501             pass
502     else:
503         try:
504              term = ApprovalTerms.objects.get(id=term_id)
505         except ApprovalTermDoesNotExist, e:
506             pass
507
508     if not term:
509         return HttpResponseRedirect(reverse('index'))
510     f = open(term.location, 'r')
511     terms = f.read()
512
513     if request.method == 'POST':
514         next = request.POST.get('next')
515         if not next:
516             next = reverse('index')
517         form = SignApprovalTermsForm(request.POST, instance=request.user)
518         if not form.is_valid():
519             return render_response(template_name,
520                            terms = terms,
521                            approval_terms_form = form,
522                            context_instance = get_context(request, extra_context))
523         user = form.save()
524         return HttpResponseRedirect(next)
525     else:
526         form = None
527         if request.user.is_authenticated() and not request.user.signed_terms():
528             form = SignApprovalTermsForm(instance=request.user)
529         return render_response(template_name,
530                                terms = terms,
531                                approval_terms_form = form,
532                                context_instance = get_context(request, extra_context))
533
534 @signed_terms_required
535 def change_password(request):
536     return password_change(request,
537                             post_change_redirect=reverse('edit_profile'),
538                             password_change_form=ExtendedPasswordChangeForm)
539
540 @signed_terms_required
541 @login_required
542 @transaction.commit_manually
543 def change_email(request, activation_key=None,
544                  email_template_name='registration/email_change_email.txt',
545                  form_template_name='registration/email_change_form.html',
546                  confirm_template_name='registration/email_change_done.html',
547                  extra_context={}):
548     if activation_key:
549         try:
550             user = EmailChange.objects.change_email(activation_key)
551             if request.user.is_authenticated() and request.user == user:
552                 msg = _('Email changed successfully.')
553                 messages.success(request, msg)
554                 auth_logout(request)
555                 response = prepare_response(request, user)
556                 transaction.commit()
557                 return response
558         except ValueError, e:
559             messages.error(request, e)
560         return render_response(confirm_template_name,
561                                modified_user = user if 'user' in locals() else None,
562                                context_instance = get_context(request,
563                                                               extra_context))
564     
565     if not request.user.is_authenticated():
566         path = quote(request.get_full_path())
567         url = request.build_absolute_uri(reverse('index'))
568         return HttpResponseRedirect(url + '?next=' + path)
569     form = EmailChangeForm(request.POST or None)
570     if request.method == 'POST' and form.is_valid():
571         try:
572             ec = form.save(email_template_name, request)
573         except SendMailError, e:
574             msg = e
575             messages.error(request, msg)
576             transaction.rollback()
577         except IntegrityError, e:
578             msg = _('There is already a pending change email request.')
579             messages.error(request, msg)
580         else:
581             msg = _('Change email request has been registered succefully.\
582                     You are going to receive a verification email in the new address.')
583             messages.success(request, msg)
584             transaction.commit()
585     return render_response(form_template_name,
586                            form = form,
587                            context_instance = get_context(request,
588                                                           extra_context))
589
590 @signed_terms_required
591 @login_required
592 def group_add(request, kind_name='default'):
593     try:
594         kind = GroupKind.objects.get(name = kind_name)
595     except:
596         return HttpResponseBadRequest(_('No such group kind'))
597     
598     template_name=None,
599     template_loader=loader
600     extra_context=None
601     post_save_redirect='/im/group/%(id)s/'
602     login_required=False
603     context_processors=None
604     model, form_class = get_model_and_form_class(
605         model=None,
606         form_class=AstakosGroupCreationForm
607     )
608     # TODO better approach???
609     resources = dict( (str(r.id), r) for r in Resource.objects.select_related().all() )
610     if request.method == 'POST':
611         form = form_class(request.POST, request.FILES, resources=resources)
612         if form.is_valid():
613             new_object = form.save()
614             new_object.owners = [request.user]
615             for (rid, limit) in form.resources():
616                 try:
617                     r = resources[rid]
618                 except KeyError, e:
619                     logger.exception(e)
620                     # Should I stay or should I go???
621                     continue
622                 else:
623                     new_object.astakosgroupquota_set.create(
624                         resource = r,
625                         limit = limit
626                     )
627             msg = _("The %(verbose_name)s was created successfully.") %\
628                                     {"verbose_name": model._meta.verbose_name}
629             messages.success(request, msg, fail_silently=True)
630             return redirect(post_save_redirect, new_object)
631     else:
632         now = datetime.now()
633         data = {
634             'kind':kind,
635             'issue_date':now,
636             'expiration_date':now + timedelta(days=30)
637         }
638         form = form_class(data, resources=resources)
639
640     # Create the template, context, response
641     template_name = "%s/%s_form.html" % (
642         model._meta.app_label,
643         model._meta.object_name.lower()
644     )
645     t = template_loader.get_template(template_name)
646     c = RequestContext(request, {
647         'form': form
648     }, context_processors)
649     return HttpResponse(t.render(c))
650
651 @signed_terms_required
652 @login_required
653 def group_list(request):
654     list = request.user.astakos_groups.select_related().all()
655     return object_list(request, queryset=list)
656
657 @signed_terms_required
658 @login_required
659 def group_detail(request, group_id):
660     try:
661         group = AstakosGroup.objects.select_related().get(id=group_id)
662     except AstakosGroup.DoesNotExist:
663         return HttpResponseBadRequest(_('Invalid group.'))
664     return object_detail(request,
665          AstakosGroup.objects.all(),
666          object_id=group_id,
667          extra_context = {'quota':group.quota}
668     )
669
670 @signed_terms_required
671 @login_required
672 def group_approval_request(request, group_id):
673     return HttpResponse()
674
675 @signed_terms_required
676 @login_required
677 def group_search(request, extra_context={}, **kwargs):
678     join_forms = {}
679     if request.method == 'GET':
680         form = AstakosGroupSearchForm()
681     else:
682         form = AstakosGroupSearchForm(get_query(request))
683         if form.is_valid():
684             q = form.cleaned_data['q'].strip()
685             q = URLField().to_python(q)
686             queryset = AstakosGroup.objects.select_related().filter(name=q)
687             f = MembershipCreationForm
688             for g in queryset:
689                 join_forms[g.name] = f(
690                     dict(
691                         group=g,
692                         person=request.user,
693                         date_requested=datetime.now().strftime("%d/%m/%Y")
694                     )
695                 )
696             return object_list(
697                 request,
698                 queryset,
699                 template_name='im/astakosgroup_list.html',
700                 extra_context=dict(
701                     form=form,
702                     is_search=True,
703                     join_forms=join_forms
704                 )
705             )
706     return render_response(
707         template='im/astakosgroup_list.html',
708         form = form,
709         context_instance=get_context(request)
710     )
711
712 @signed_terms_required
713 @login_required
714 def group_join(request, group_id):
715     return create_object(
716         request,
717         model=Membership,
718         template_name='im/astakosgroup_list.html',
719         post_save_redirect = reverse(
720             'group_detail',
721             kwargs=dict(group_id=group_id)
722         )
723     )
724
725 @signed_terms_required
726 @login_required
727 def group_leave(request, group_id):
728     try:
729         m = Membership.objects.select_related().get(
730             group__id=group_id,
731             person=request.user
732         )
733     except Membership.DoesNotExist:
734         return HttpResponseBadRequest(_('Invalid membership.'))
735     if request.user in m.group.owner.all():
736         return HttpResponseForbidden(_('Owner can not leave the group.'))
737     return delete_object(
738         request,
739         model=Membership,
740         object_id = m.id,
741         template_name='im/astakosgroup_list.html',
742         post_delete_redirect = reverse(
743             'group_detail',
744             kwargs=dict(group_id=group_id)
745         )
746     )
747
748 def handle_membership():
749     def decorator(func):
750         @wraps(func)
751         def wrapper(request, group_id, user_id):
752             try:
753                 m = Membership.objects.select_related().get(
754                     group__id=group_id,
755                     person__id=user_id
756                 )
757             except Membership.DoesNotExist:
758                 return HttpResponseBadRequest(_('Invalid membership.'))
759             else:
760                 if request.user not in m.group.owner.all():
761                     return HttpResponseForbidden(_('User is not a group owner.'))
762                 func(request, m)
763                 return render_response(
764                     template='im/astakosgroup_detail.html',
765                     context_instance=get_context(request),
766                     object=m.group,
767                     quota=m.group.quota,
768                     more_policies=m.group.has_undefined_policies
769                 )
770         return wrapper
771     return decorator
772
773 @signed_terms_required
774 @login_required
775 @handle_membership()
776 def approve_member(request, membership):
777     try:
778         membership.approve()
779         realname = membership.person.realname
780         msg = _('%s has been successfully joined the group.' % realname)
781         messages.success(request, msg)
782     except BaseException, e:
783         logger.exception(e)
784         msg = _('Something went wrong during %s\'s approval.' % realname)
785         messages.error(request, msg)
786     
787 @signed_terms_required
788 @login_required
789 @handle_membership()
790 def disapprove_member(request, membership):
791     try:
792         membership.disapprove()
793         realname = membership.person.realname
794         msg = _('%s has been successfully removed from the group.' % realname)
795         messages.success(request, msg)
796     except BaseException, e:
797         logger.exception(e)
798         msg = _('Something went wrong during %s\'s disapproval.' % realname)
799         messages.error(request, msg)
800
801 @signed_terms_required
802 @login_required
803 def resource_list(request):
804     return render_response(
805         template='im/astakosuserquota_list.html',
806         context_instance=get_context(request),
807         quota=request.user.quota
808     )