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