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