Do not show default groups in project lists
[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 import inflect
37
38 engine = inflect.engine()
39
40 from urllib import quote
41 from functools import wraps
42 from datetime import datetime
43
44 from django.contrib import messages
45 from django.contrib.auth.decorators import login_required
46 from django.core.urlresolvers import reverse
47 from django.db import transaction
48 from django.db.utils import IntegrityError
49 from django.http import (HttpResponse, HttpResponseBadRequest,
50                          HttpResponseForbidden, HttpResponseRedirect,
51                          HttpResponseBadRequest, Http404)
52 from django.shortcuts import redirect
53 from django.template import RequestContext, loader as template_loader
54 from django.utils.http import urlencode
55 from django.utils.translation import ugettext as _
56 from django.views.generic.create_update import (delete_object,
57                                                 get_model_and_form_class)
58 from django.views.generic.list_detail import object_list
59 from django.core.xheaders import populate_xheaders
60 from django.core.exceptions import ValidationError, PermissionDenied
61 from django.template.loader import render_to_string
62 from django.views.decorators.http import require_http_methods
63 from django.db.models import Q
64
65 from astakos.im.activation_backends import get_backend, SimpleBackend
66 from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
67                                EmailChange, GroupKind, Membership,
68                                RESOURCE_SEPARATOR, AstakosUserAuthProvider)
69 from astakos.im.util import get_context, prepare_response, get_query, restrict_next
70 from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
71                               FeedbackForm, SignApprovalTermsForm,
72                               EmailChangeForm,
73                               AstakosGroupCreationForm, AstakosGroupSearchForm,
74                               AstakosGroupUpdateForm, AddGroupMembersForm,
75                               MembersSortForm, AstakosGroupSortForm,
76                               TimelineForm, PickResourceForm,
77                               AstakosGroupCreationSummaryForm)
78 from astakos.im.functions import (send_feedback, SendMailError,
79                                   logout as auth_logout,
80                                   activate as activate_func,
81                                   send_activation as send_activation_func,
82                                   send_group_creation_notification,
83                                   SendNotificationError)
84 from astakos.im.endpoints.qh import timeline_charge
85 from astakos.im.settings import (COOKIE_DOMAIN, LOGOUT_NEXT,
86                                  LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
87 #from astakos.im.tasks import request_billing
88 from astakos.im.api.callpoint import AstakosCallpoint
89
90 import astakos.im.messages as astakos_messages
91 from astakos.im import settings
92 from astakos.im import auth_providers
93
94 logger = logging.getLogger(__name__)
95
96 callpoint = AstakosCallpoint()
97
98 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
99     """
100     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
101     keyword argument and returns an ``django.http.HttpResponse`` with the
102     specified ``status``.
103     """
104     if tab is None:
105         tab = template.partition('_')[0].partition('.html')[0]
106     kwargs.setdefault('tab', tab)
107     html = template_loader.render_to_string(
108         template, kwargs, context_instance=context_instance)
109     response = HttpResponse(html, status=status)
110     return response
111
112 def requires_auth_provider(provider_id, **perms):
113     """
114     """
115     def decorator(func, *args, **kwargs):
116         @wraps(func)
117         def wrapper(request, *args, **kwargs):
118             provider = auth_providers.get_provider(provider_id)
119
120             if not provider or not provider.is_active():
121                 raise PermissionDenied
122
123             if provider:
124                 for pkey, value in perms.iteritems():
125                     attr = 'is_available_for_%s' % pkey.lower()
126                     if getattr(provider, attr)() != value:
127                         raise PermissionDenied
128             return func(request, *args)
129         return wrapper
130     return decorator
131
132
133 def requires_anonymous(func):
134     """
135     Decorator checkes whether the request.user is not Anonymous and in that case
136     redirects to `logout`.
137     """
138     @wraps(func)
139     def wrapper(request, *args):
140         if not request.user.is_anonymous():
141             next = urlencode({'next': request.build_absolute_uri()})
142             logout_uri = reverse(logout) + '?' + next
143             return HttpResponseRedirect(logout_uri)
144         return func(request, *args)
145     return wrapper
146
147
148 def signed_terms_required(func):
149     """
150     Decorator checkes whether the request.user is Anonymous and in that case
151     redirects to `logout`.
152     """
153     @wraps(func)
154     def wrapper(request, *args, **kwargs):
155         if request.user.is_authenticated() and not request.user.signed_terms:
156             params = urlencode({'next': request.build_absolute_uri(),
157                                 'show_form': ''})
158             terms_uri = reverse('latest_terms') + '?' + params
159             return HttpResponseRedirect(terms_uri)
160         return func(request, *args, **kwargs)
161     return wrapper
162
163
164 @require_http_methods(["GET", "POST"])
165 @signed_terms_required
166 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
167     """
168     If there is logged on user renders the profile page otherwise renders login page.
169
170     **Arguments**
171
172     ``login_template_name``
173         A custom login template to use. This is optional; if not specified,
174         this will default to ``im/login.html``.
175
176     ``profile_template_name``
177         A custom profile template to use. This is optional; if not specified,
178         this will default to ``im/profile.html``.
179
180     ``extra_context``
181         An dictionary of variables to add to the template context.
182
183     **Template:**
184
185     im/profile.html or im/login.html or ``template_name`` keyword argument.
186
187     """
188     extra_context = extra_context or {}
189     template_name = login_template_name
190     if request.user.is_authenticated():
191         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
192
193     return render_response(
194         template_name,
195         login_form = LoginForm(request=request),
196         context_instance = get_context(request, extra_context)
197     )
198
199
200 @require_http_methods(["GET", "POST"])
201 @login_required
202 @signed_terms_required
203 @transaction.commit_manually
204 def invite(request, template_name='im/invitations.html', extra_context=None):
205     """
206     Allows a user to invite somebody else.
207
208     In case of GET request renders a form for providing the invitee information.
209     In case of POST checks whether the user has not run out of invitations and then
210     sends an invitation email to singup to the service.
211
212     The view uses commit_manually decorator in order to ensure the number of the
213     user invitations is going to be updated only if the email has been successfully sent.
214
215     If the user isn't logged in, redirects to settings.LOGIN_URL.
216
217     **Arguments**
218
219     ``template_name``
220         A custom template to use. This is optional; if not specified,
221         this will default to ``im/invitations.html``.
222
223     ``extra_context``
224         An dictionary of variables to add to the template context.
225
226     **Template:**
227
228     im/invitations.html or ``template_name`` keyword argument.
229
230     **Settings:**
231
232     The view expectes the following settings are defined:
233
234     * LOGIN_URL: login uri
235     """
236     extra_context = extra_context or {}
237     status = None
238     message = None
239     form = InvitationForm()
240
241     inviter = request.user
242     if request.method == 'POST':
243         form = InvitationForm(request.POST)
244         if inviter.invitations > 0:
245             if form.is_valid():
246                 try:
247                     email = form.cleaned_data.get('username')
248                     realname = form.cleaned_data.get('realname')
249                     inviter.invite(email, realname)
250                     message = _(astakos_messages.INVITATION_SENT) % locals()
251                     messages.success(request, message)
252                 except SendMailError, e:
253                     message = e.message
254                     messages.error(request, message)
255                     transaction.rollback()
256                 except BaseException, e:
257                     message = _(astakos_messages.GENERIC_ERROR)
258                     messages.error(request, message)
259                     logger.exception(e)
260                     transaction.rollback()
261                 else:
262                     transaction.commit()
263         else:
264             message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
265             messages.error(request, message)
266
267     sent = [{'email': inv.username,
268              'realname': inv.realname,
269              'is_consumed': inv.is_consumed}
270             for inv in request.user.invitations_sent.all()]
271     kwargs = {'inviter': inviter,
272               'sent': sent}
273     context = get_context(request, extra_context, **kwargs)
274     return render_response(template_name,
275                            invitation_form=form,
276                            context_instance=context)
277
278
279 @require_http_methods(["GET", "POST"])
280 @login_required
281 @signed_terms_required
282 def edit_profile(request, template_name='im/profile.html', extra_context=None):
283     """
284     Allows a user to edit his/her profile.
285
286     In case of GET request renders a form for displaying the user information.
287     In case of POST updates the user informantion and redirects to ``next``
288     url parameter if exists.
289
290     If the user isn't logged in, redirects to settings.LOGIN_URL.
291
292     **Arguments**
293
294     ``template_name``
295         A custom template to use. This is optional; if not specified,
296         this will default to ``im/profile.html``.
297
298     ``extra_context``
299         An dictionary of variables to add to the template context.
300
301     **Template:**
302
303     im/profile.html or ``template_name`` keyword argument.
304
305     **Settings:**
306
307     The view expectes the following settings are defined:
308
309     * LOGIN_URL: login uri
310     """
311     extra_context = extra_context or {}
312     form = ProfileForm(
313         instance=request.user,
314         session_key=request.session.session_key
315     )
316     extra_context['next'] = request.GET.get('next')
317     if request.method == 'POST':
318         form = ProfileForm(
319             request.POST,
320             instance=request.user,
321             session_key=request.session.session_key
322         )
323         if form.is_valid():
324             try:
325                 prev_token = request.user.auth_token
326                 user = form.save()
327                 form = ProfileForm(
328                     instance=user,
329                     session_key=request.session.session_key
330                 )
331                 next = restrict_next(
332                     request.POST.get('next'),
333                     domain=COOKIE_DOMAIN
334                 )
335                 if next:
336                     return redirect(next)
337                 msg = _(astakos_messages.PROFILE_UPDATED)
338                 messages.success(request, msg)
339             except ValueError, ve:
340                 messages.success(request, ve)
341     elif request.method == "GET":
342         request.user.is_verified = True
343         request.user.save()
344
345     # existing providers
346     user_providers = request.user.get_active_auth_providers()
347
348     # providers that user can add
349     user_available_providers = request.user.get_available_auth_providers()
350
351     return render_response(template_name,
352                            profile_form = form,
353                            user_providers = user_providers,
354                            user_available_providers = user_available_providers,
355                            context_instance = get_context(request,
356                                                           extra_context))
357
358
359 @transaction.commit_manually
360 @require_http_methods(["GET", "POST"])
361 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
362     """
363     Allows a user to create a local account.
364
365     In case of GET request renders a form for entering the user information.
366     In case of POST handles the signup.
367
368     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
369     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
370     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
371     (see activation_backends);
372
373     Upon successful user creation, if ``next`` url parameter is present the user is redirected there
374     otherwise renders the same page with a success message.
375
376     On unsuccessful creation, renders ``template_name`` with an error message.
377
378     **Arguments**
379
380     ``template_name``
381         A custom template to render. This is optional;
382         if not specified, this will default to ``im/signup.html``.
383
384     ``on_success``
385         A custom template to render in case of success. This is optional;
386         if not specified, this will default to ``im/signup_complete.html``.
387
388     ``extra_context``
389         An dictionary of variables to add to the template context.
390
391     **Template:**
392
393     im/signup.html or ``template_name`` keyword argument.
394     im/signup_complete.html or ``on_success`` keyword argument.
395     """
396     extra_context = extra_context or {}
397     if request.user.is_authenticated():
398         return HttpResponseRedirect(reverse('edit_profile'))
399
400     provider = get_query(request).get('provider', 'local')
401     if not auth_providers.get_provider(provider).is_available_for_create():
402         raise PermissionDenied
403
404     id = get_query(request).get('id')
405     try:
406         instance = AstakosUser.objects.get(id=id) if id else None
407     except AstakosUser.DoesNotExist:
408         instance = None
409
410     third_party_token = request.REQUEST.get('third_party_token', None)
411
412     try:
413         if not backend:
414             backend = get_backend(request)
415         form = backend.get_signup_form(provider, instance)
416     except Exception, e:
417         form = SimpleBackend(request).get_signup_form(provider)
418         messages.error(request, e)
419     if request.method == 'POST':
420         if form.is_valid():
421             user = form.save(commit=False)
422             try:
423                 result = backend.handle_activation(user)
424                 status = messages.SUCCESS
425                 message = result.message
426
427                 form.store_user(user, request)
428
429                 if 'additional_email' in form.cleaned_data:
430                     additional_email = form.cleaned_data['additional_email']
431                     if additional_email != user.email:
432                         user.additionalmail_set.create(email=additional_email)
433                         msg = 'Additional email: %s saved for user %s.' % (
434                             additional_email,
435                             user.email
436                         )
437                         logger._log(LOGGING_LEVEL, msg, [])
438                 if user and user.is_active:
439                     next = request.POST.get('next', '')
440                     response = prepare_response(request, user, next=next)
441                     transaction.commit()
442                     return response
443                 messages.add_message(request, status, message)
444                 transaction.commit()
445                 return render_response(
446                     on_success,
447                     context_instance=get_context(
448                         request,
449                         extra_context
450                     )
451                 )
452             except SendMailError, e:
453                 logger.exception(e)
454                 status = messages.ERROR
455                 message = e.message
456                 messages.error(request, message)
457                 transaction.rollback()
458             except BaseException, e:
459                 logger.exception(e)
460                 message = _(astakos_messages.GENERIC_ERROR)
461                 messages.error(request, message)
462                 logger.exception(e)
463                 transaction.rollback()
464     return render_response(template_name,
465                            signup_form=form,
466                            third_party_token=third_party_token,
467                            provider=provider,
468                            context_instance=get_context(request, extra_context))
469
470
471 @require_http_methods(["GET", "POST"])
472 @login_required
473 @signed_terms_required
474 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
475     """
476     Allows a user to send feedback.
477
478     In case of GET request renders a form for providing the feedback information.
479     In case of POST sends an email to support team.
480
481     If the user isn't logged in, redirects to settings.LOGIN_URL.
482
483     **Arguments**
484
485     ``template_name``
486         A custom template to use. This is optional; if not specified,
487         this will default to ``im/feedback.html``.
488
489     ``extra_context``
490         An dictionary of variables to add to the template context.
491
492     **Template:**
493
494     im/signup.html or ``template_name`` keyword argument.
495
496     **Settings:**
497
498     * LOGIN_URL: login uri
499     """
500     extra_context = extra_context or {}
501     if request.method == 'GET':
502         form = FeedbackForm()
503     if request.method == 'POST':
504         if not request.user:
505             return HttpResponse('Unauthorized', status=401)
506
507         form = FeedbackForm(request.POST)
508         if form.is_valid():
509             msg = form.cleaned_data['feedback_msg']
510             data = form.cleaned_data['feedback_data']
511             try:
512                 send_feedback(msg, data, request.user, email_template_name)
513             except SendMailError, e:
514                 messages.error(request, message)
515             else:
516                 message = _(astakos_messages.FEEDBACK_SENT)
517                 messages.success(request, message)
518     return render_response(template_name,
519                            feedback_form=form,
520                            context_instance=get_context(request, extra_context))
521
522
523 @require_http_methods(["GET"])
524 @signed_terms_required
525 def logout(request, template='registration/logged_out.html', extra_context=None):
526     """
527     Wraps `django.contrib.auth.logout`.
528     """
529     extra_context = extra_context or {}
530     response = HttpResponse()
531     if request.user.is_authenticated():
532         email = request.user.email
533         auth_logout(request)
534     next = restrict_next(
535         request.GET.get('next'),
536         domain=COOKIE_DOMAIN
537     )
538     if next:
539         response['Location'] = next
540         response.status_code = 302
541     elif LOGOUT_NEXT:
542         response['Location'] = LOGOUT_NEXT
543         response.status_code = 301
544     else:
545         messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
546         context = get_context(request, extra_context)
547         response.write(render_to_string(template, context_instance=context))
548     return response
549
550
551 @require_http_methods(["GET", "POST"])
552 @transaction.commit_manually
553 def activate(request, greeting_email_template_name='im/welcome_email.txt',
554              helpdesk_email_template_name='im/helpdesk_notification.txt'):
555     """
556     Activates the user identified by the ``auth`` request parameter, sends a welcome email
557     and renews the user token.
558
559     The view uses commit_manually decorator in order to ensure the user state will be updated
560     only if the email will be send successfully.
561     """
562     token = request.GET.get('auth')
563     next = request.GET.get('next')
564     try:
565         user = AstakosUser.objects.get(auth_token=token)
566     except AstakosUser.DoesNotExist:
567         return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
568
569     if user.is_active:
570         message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
571         messages.error(request, message)
572         return index(request)
573
574     try:
575         activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
576         response = prepare_response(request, user, next, renew=True)
577         transaction.commit()
578         return response
579     except SendMailError, e:
580         message = e.message
581         messages.add_message(request, messages.ERROR, message)
582         transaction.rollback()
583         return index(request)
584     except BaseException, e:
585         status = messages.ERROR
586         message = _(astakos_messages.GENERIC_ERROR)
587         messages.add_message(request, messages.ERROR, message)
588         logger.exception(e)
589         transaction.rollback()
590         return index(request)
591
592
593 @require_http_methods(["GET", "POST"])
594 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
595     extra_context = extra_context or {}
596     term = None
597     terms = None
598     if not term_id:
599         try:
600             term = ApprovalTerms.objects.order_by('-id')[0]
601         except IndexError:
602             pass
603     else:
604         try:
605             term = ApprovalTerms.objects.get(id=term_id)
606         except ApprovalTerms.DoesNotExist, e:
607             pass
608
609     if not term:
610         messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
611         return HttpResponseRedirect(reverse('index'))
612     f = open(term.location, 'r')
613     terms = f.read()
614
615     if request.method == 'POST':
616         next = restrict_next(
617             request.POST.get('next'),
618             domain=COOKIE_DOMAIN
619         )
620         if not next:
621             next = reverse('index')
622         form = SignApprovalTermsForm(request.POST, instance=request.user)
623         if not form.is_valid():
624             return render_response(template_name,
625                                    terms=terms,
626                                    approval_terms_form=form,
627                                    context_instance=get_context(request, extra_context))
628         user = form.save()
629         return HttpResponseRedirect(next)
630     else:
631         form = None
632         if request.user.is_authenticated() and not request.user.signed_terms:
633             form = SignApprovalTermsForm(instance=request.user)
634         return render_response(template_name,
635                                terms=terms,
636                                approval_terms_form=form,
637                                context_instance=get_context(request, extra_context))
638
639
640 @require_http_methods(["GET", "POST"])
641 @login_required
642 @signed_terms_required
643 @transaction.commit_manually
644 def change_email(request, activation_key=None,
645                  email_template_name='registration/email_change_email.txt',
646                  form_template_name='registration/email_change_form.html',
647                  confirm_template_name='registration/email_change_done.html',
648                  extra_context=None):
649     extra_context = extra_context or {}
650     if activation_key:
651         try:
652             user = EmailChange.objects.change_email(activation_key)
653             if request.user.is_authenticated() and request.user == user:
654                 msg = _(astakos_messages.EMAIL_CHANGED)
655                 messages.success(request, msg)
656                 auth_logout(request)
657                 response = prepare_response(request, user)
658                 transaction.commit()
659                 return response
660         except ValueError, e:
661             messages.error(request, e)
662         return render_response(confirm_template_name,
663                                modified_user=user if 'user' in locals(
664                                ) else None,
665                                context_instance=get_context(request,
666                                                             extra_context))
667
668     if not request.user.is_authenticated():
669         path = quote(request.get_full_path())
670         url = request.build_absolute_uri(reverse('index'))
671         return HttpResponseRedirect(url + '?next=' + path)
672     form = EmailChangeForm(request.POST or None)
673     if request.method == 'POST' and form.is_valid():
674         try:
675             ec = form.save(email_template_name, request)
676         except SendMailError, e:
677             msg = e
678             messages.error(request, msg)
679             transaction.rollback()
680         except IntegrityError, e:
681             msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
682             messages.error(request, msg)
683         else:
684             msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
685             messages.success(request, msg)
686             transaction.commit()
687     return render_response(
688         form_template_name,
689         form=form,
690         context_instance=get_context(request, extra_context)
691     )
692
693
694 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
695
696     if settings.MODERATION_ENABLED:
697         raise PermissionDenied
698
699     extra_context = extra_context or {}
700     try:
701         u = AstakosUser.objects.get(id=user_id)
702     except AstakosUser.DoesNotExist:
703         messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
704     else:
705         try:
706             send_activation_func(u)
707             msg = _(astakos_messages.ACTIVATION_SENT)
708             messages.success(request, msg)
709         except SendMailError, e:
710             messages.error(request, e)
711     return render_response(
712         template_name,
713         login_form = LoginForm(request=request),
714         context_instance = get_context(
715             request,
716             extra_context
717         )
718     )
719
720 class ResourcePresentation():
721
722     def __init__(self, data):
723         self.data = data
724
725     def update_from_result(self, result):
726         if result.is_success:
727             for r in result.data:
728                 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
729                 if not rname in self.data['resources']:
730                     self.data['resources'][rname] = {}
731
732                 self.data['resources'][rname].update(r)
733                 self.data['resources'][rname]['id'] = rname
734                 group = r.get('group')
735                 if not group in self.data['groups']:
736                     self.data['groups'][group] = {}
737
738                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
739
740     def test(self, quota_dict):
741         for k, v in quota_dict.iteritems():
742             rname = k
743             value = v
744             if not rname in self.data['resources']:
745                 self.data['resources'][rname] = {}
746
747
748             self.data['resources'][rname]['value'] = value
749
750
751     def update_from_result_report(self, result):
752         if result.is_success:
753             for r in result.data:
754                 rname = r.get('name')
755                 if not rname in self.data['resources']:
756                     self.data['resources'][rname] = {}
757
758                 self.data['resources'][rname].update(r)
759                 self.data['resources'][rname]['id'] = rname
760                 group = r.get('group')
761                 if not group in self.data['groups']:
762                     self.data['groups'][group] = {}
763
764                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
765
766     def get_group_resources(self, group):
767         return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
768
769     def get_groups_resources(self):
770         for g in self.data['groups']:
771             yield g, self.get_group_resources(g)
772
773     def get_quota(self, group_quotas):
774         for r, v in group_quotas.iteritems():
775             rname = str(r)
776             quota = self.data['resources'].get(rname)
777             quota['value'] = v
778             yield quota
779
780
781     def get_policies(self, policies_data):
782         for policy in policies_data:
783             rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
784             policy.update(self.data['resources'].get(rname))
785             yield policy
786
787     def __repr__(self):
788         return self.data.__repr__()
789
790     def __iter__(self, *args, **kwargs):
791         return self.data.__iter__(*args, **kwargs)
792
793     def __getitem__(self, *args, **kwargs):
794         return self.data.__getitem__(*args, **kwargs)
795
796     def get(self, *args, **kwargs):
797         return self.data.get(*args, **kwargs)
798
799
800
801 @require_http_methods(["GET", "POST"])
802 @signed_terms_required
803 @login_required
804 def group_add(request, kind_name='default'):
805
806     result = callpoint.list_resources()
807     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
808     resource_catalog.update_from_result(result)
809
810     if not result.is_success:
811         messages.error(
812             request,
813             'Unable to retrieve system resources: %s' % result.reason
814     )
815
816     try:
817         kind = GroupKind.objects.get(name=kind_name)
818     except:
819         return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
820
821
822
823     post_save_redirect = '/im/group/%(id)s/'
824     context_processors = None
825     model, form_class = get_model_and_form_class(
826         model=None,
827         form_class=AstakosGroupCreationForm
828     )
829
830     if request.method == 'POST':
831         form = form_class(request.POST, request.FILES)
832         if form.is_valid():
833             policies = form.policies()
834             return render_response(
835                 template='im/astakosgroup_form_summary.html',
836                 context_instance=get_context(request),
837                 form=AstakosGroupCreationSummaryForm(form.cleaned_data),
838                 policies=resource_catalog.get_policies(policies)
839             )
840     else:
841         now = datetime.now()
842         data = {
843             'kind': kind,
844         }
845         for group, resources in resource_catalog.get_groups_resources():
846             data['is_selected_%s' % group] = False
847             for resource in resources:
848                 data['%s_uplimit' % resource] = ''
849
850         form = form_class(data)
851
852     # Create the template, context, response
853     template_name = "%s/%s_form.html" % (
854         model._meta.app_label,
855         model._meta.object_name.lower()
856     )
857     t = template_loader.get_template(template_name)
858     c = RequestContext(request, {
859         'form': form,
860         'kind': kind,
861         'resource_catalog':resource_catalog,
862     }, context_processors)
863     return HttpResponse(t.render(c))
864
865
866 #@require_http_methods(["POST"])
867 @require_http_methods(["GET", "POST"])
868 @signed_terms_required
869 @login_required
870 def group_add_complete(request):
871     model = AstakosGroup
872     form = AstakosGroupCreationSummaryForm(request.POST)
873     if form.is_valid():
874         d = form.cleaned_data
875         d['owners'] = [request.user]
876         result = callpoint.create_groups((d,)).next()
877         if result.is_success:
878             new_object = result.data[0]
879             msg = _(astakos_messages.OBJECT_CREATED) %\
880                 {"verbose_name": model._meta.verbose_name}
881             messages.success(request, msg, fail_silently=True)
882
883             # send notification
884             try:
885                 send_group_creation_notification(
886                     template_name='im/group_creation_notification.txt',
887                     dictionary={
888                         'group': new_object,
889                         'owner': request.user,
890                         'policies': d.get('policies', [])
891                     }
892                 )
893             except SendNotificationError, e:
894                 messages.error(request, e, fail_silently=True)
895             post_save_redirect = '/im/group/%(id)s/'
896             return HttpResponseRedirect(post_save_redirect % new_object)
897         else:
898             d = {"verbose_name": model._meta.verbose_name,
899                  "reason":result.reason}
900             msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
901             messages.error(request, msg, fail_silently=True)
902     return render_response(
903         template='im/astakosgroup_form_summary.html',
904         context_instance=get_context(request),
905         form=form,
906         policies=form.cleaned_data.get('policies')
907     )
908
909
910 #@require_http_methods(["GET"])
911 @require_http_methods(["GET", "POST"])
912 @signed_terms_required
913 @login_required
914 def group_list(request):
915     none = request.user.astakos_groups.none()
916     query = """
917         SELECT auth_group.id,
918         auth_group.name AS groupname,
919         im_groupkind.name AS kindname,
920         im_astakosgroup.*,
921         owner.email AS groupowner,
922         (SELECT COUNT(*) FROM im_membership
923             WHERE group_id = im_astakosgroup.group_ptr_id
924             AND date_joined IS NOT NULL) AS approved_members_num,
925         (SELECT CASE WHEN(
926                     SELECT date_joined FROM im_membership
927                     WHERE group_id = im_astakosgroup.group_ptr_id
928                     AND person_id = %(userid)s) IS NULL
929                     THEN 0 ELSE 1 END) AS membership_status
930         FROM im_astakosgroup
931         INNER JOIN im_membership ON (
932             im_astakosgroup.group_ptr_id = im_membership.group_id)
933         INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
934         INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
935         LEFT JOIN im_astakosuser_owner ON (
936             im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
937         LEFT JOIN auth_user as owner ON (
938             im_astakosuser_owner.astakosuser_id = owner.id)
939         WHERE im_membership.person_id = %(userid)s
940         AND im_groupkind.name != 'default'
941         """
942     params = {'userid':request.user.id}
943
944     # validate sorting
945     sorting = 'groupname'
946     sort_form = AstakosGroupSortForm(request.GET)
947     if sort_form.is_valid():
948         sorting = sort_form.cleaned_data.get('sorting')
949     query = query+" ORDER BY %s ASC" %sorting
950     
951     q = AstakosGroup.objects.raw(query, params=params)
952     
953     # Create the template, context, response
954     template_name = "%s/%s_list.html" % (
955         q.model._meta.app_label,
956         q.model._meta.object_name.lower()
957     )
958     extra_context = dict(
959         is_search=False,
960         q=q,
961         sorting=sorting,
962         page=request.GET.get('page', 1)
963     )
964     return render_response(template_name,
965                            context_instance=get_context(request, extra_context)
966     )
967
968
969 @require_http_methods(["GET", "POST"])
970 @signed_terms_required
971 @login_required
972 def group_detail(request, group_id):
973     q = AstakosGroup.objects.select_related().filter(pk=group_id)
974     q = q.extra(select={
975         'is_member': """SELECT CASE WHEN EXISTS(
976                             SELECT id FROM im_membership
977                             WHERE group_id = im_astakosgroup.group_ptr_id
978                             AND person_id = %s)
979                         THEN 1 ELSE 0 END""" % request.user.id,
980         'is_owner': """SELECT CASE WHEN EXISTS(
981                         SELECT id FROM im_astakosuser_owner
982                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
983                         AND astakosuser_id = %s)
984                         THEN 1 ELSE 0 END""" % request.user.id,
985         'kindname': """SELECT name FROM im_groupkind
986                        WHERE id = im_astakosgroup.kind_id"""})
987
988     model = q.model
989     context_processors = None
990     mimetype = None
991     try:
992         obj = q.get()
993     except AstakosGroup.DoesNotExist:
994         raise Http404("No %s found matching the query" % (
995             model._meta.verbose_name))
996
997     update_form = AstakosGroupUpdateForm(instance=obj)
998     addmembers_form = AddGroupMembersForm()
999     if request.method == 'POST':
1000         update_data = {}
1001         addmembers_data = {}
1002         for k, v in request.POST.iteritems():
1003             if k in update_form.fields:
1004                 update_data[k] = v
1005             if k in addmembers_form.fields:
1006                 addmembers_data[k] = v
1007         update_data = update_data or None
1008         addmembers_data = addmembers_data or None
1009         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1010         addmembers_form = AddGroupMembersForm(addmembers_data)
1011         if update_form.is_valid():
1012             update_form.save()
1013         if addmembers_form.is_valid():
1014             try:
1015                 map(obj.approve_member, addmembers_form.valid_users)
1016             except AssertionError:
1017                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1018                 messages.error(request, msg)
1019             addmembers_form = AddGroupMembersForm()
1020
1021     template_name = "%s/%s_detail.html" % (
1022         model._meta.app_label, model._meta.object_name.lower())
1023     t = template_loader.get_template(template_name)
1024     c = RequestContext(request, {
1025         'object': obj,
1026     }, context_processors)
1027
1028     # validate sorting
1029     sorting = 'person__email'
1030     form = MembersSortForm(request.GET)
1031     if form.is_valid():
1032         sorting = form.cleaned_data.get('sorting')
1033     
1034     result = callpoint.list_resources()
1035     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1036     resource_catalog.update_from_result(result)
1037
1038
1039     if not result.is_success:
1040         messages.error(
1041             request,
1042             'Unable to retrieve system resources: %s' % result.reason
1043     )
1044
1045     extra_context = {'update_form': update_form,
1046                      'addmembers_form': addmembers_form,
1047                      'page': request.GET.get('page', 1),
1048                      'sorting': sorting,
1049                      'resource_catalog':resource_catalog,
1050                      'quota':resource_catalog.get_quota(obj.quota)}
1051     for key, value in extra_context.items():
1052         if callable(value):
1053             c[key] = value()
1054         else:
1055             c[key] = value
1056     response = HttpResponse(t.render(c), mimetype=mimetype)
1057     populate_xheaders(
1058         request, response, model, getattr(obj, obj._meta.pk.name))
1059     return response
1060
1061
1062 @require_http_methods(["GET", "POST"])
1063 @signed_terms_required
1064 @login_required
1065 def group_search(request, extra_context=None, **kwargs):
1066     q = request.GET.get('q')
1067     if request.method == 'GET':
1068         form = AstakosGroupSearchForm({'q': q} if q else None)
1069     else:
1070         form = AstakosGroupSearchForm(get_query(request))
1071         if form.is_valid():
1072             q = form.cleaned_data['q'].strip()
1073     
1074     sorting = 'groupname'
1075     if q:
1076         queryset = AstakosGroup.objects.select_related()
1077         queryset = queryset.filter(~Q(kind__name='default'))
1078         queryset = queryset.filter(name__contains=q)
1079         queryset = queryset.filter(approval_date__isnull=False)
1080         queryset = queryset.extra(select={
1081                                   'groupname': "auth_group.name",
1082                                   'kindname': "im_groupkind.name",
1083                                   'approved_members_num': """
1084                     SELECT COUNT(*) FROM im_membership
1085                     WHERE group_id = im_astakosgroup.group_ptr_id
1086                     AND date_joined IS NOT NULL""",
1087                                   'membership_approval_date': """
1088                     SELECT date_joined FROM im_membership
1089                     WHERE group_id = im_astakosgroup.group_ptr_id
1090                     AND person_id = %s""" % request.user.id,
1091                                   'is_member': """
1092                     SELECT CASE WHEN EXISTS(
1093                     SELECT date_joined FROM im_membership
1094                     WHERE group_id = im_astakosgroup.group_ptr_id
1095                     AND person_id = %s)
1096                     THEN 1 ELSE 0 END""" % request.user.id,
1097                                   'is_owner': """
1098                     SELECT CASE WHEN EXISTS(
1099                     SELECT id FROM im_astakosuser_owner
1100                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1101                     AND astakosuser_id = %s)
1102                     THEN 1 ELSE 0 END""" % request.user.id,
1103                     'is_owner': """SELECT CASE WHEN EXISTS(
1104                         SELECT id FROM im_astakosuser_owner
1105                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1106                         AND astakosuser_id = %s)
1107                         THEN 1 ELSE 0 END""" % request.user.id,
1108                     })
1109         
1110         # validate sorting
1111         sort_form = AstakosGroupSortForm(request.GET)
1112         if sort_form.is_valid():
1113             sorting = sort_form.cleaned_data.get('sorting')
1114         queryset = queryset.order_by(sorting)
1115
1116     else:
1117         queryset = AstakosGroup.objects.none()
1118     return object_list(
1119         request,
1120         queryset,
1121         paginate_by=PAGINATE_BY_ALL,
1122         page=request.GET.get('page') or 1,
1123         template_name='im/astakosgroup_list.html',
1124         extra_context=dict(form=form,
1125                            is_search=True,
1126                            q=q,
1127                            sorting=sorting))
1128
1129
1130 @require_http_methods(["GET", "POST"])
1131 @signed_terms_required
1132 @login_required
1133 def group_all(request, extra_context=None, **kwargs):
1134     q = AstakosGroup.objects.select_related()
1135     q = q.filter(~Q(kind__name='default'))
1136     q = q.filter(approval_date__isnull=False)
1137     q = q.extra(select={
1138                 'groupname': "auth_group.name",
1139                 'kindname': "im_groupkind.name",
1140                 'approved_members_num': """
1141                     SELECT COUNT(*) FROM im_membership
1142                     WHERE group_id = im_astakosgroup.group_ptr_id
1143                     AND date_joined IS NOT NULL""",
1144                 'membership_approval_date': """
1145                     SELECT date_joined FROM im_membership
1146                     WHERE group_id = im_astakosgroup.group_ptr_id
1147                     AND person_id = %s""" % request.user.id,
1148                 'is_member': """
1149                     SELECT CASE WHEN EXISTS(
1150                     SELECT date_joined FROM im_membership
1151                     WHERE group_id = im_astakosgroup.group_ptr_id
1152                     AND person_id = %s)
1153                     THEN 1 ELSE 0 END""" % request.user.id,
1154                  'is_owner': """SELECT CASE WHEN EXISTS(
1155                         SELECT id FROM im_astakosuser_owner
1156                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1157                         AND astakosuser_id = %s)
1158                         THEN 1 ELSE 0 END""" % request.user.id,   })
1159     
1160     # validate sorting
1161     sorting = 'groupname'
1162     print '>>>', sorting, request.GET
1163     sort_form = AstakosGroupSortForm(request.GET)
1164     if sort_form.is_valid():
1165         sorting = sort_form.cleaned_data.get('sorting')
1166     print '<<<', sorting
1167     q = q.order_by(sorting)
1168     
1169     return object_list(
1170         request,
1171         q,
1172         paginate_by=PAGINATE_BY_ALL,
1173         page=request.GET.get('page') or 1,
1174         template_name='im/astakosgroup_list.html',
1175         extra_context=dict(form=AstakosGroupSearchForm(),
1176                            is_search=True,
1177                            sorting=sorting))
1178
1179
1180 #@require_http_methods(["POST"])
1181 @require_http_methods(["POST", "GET"])
1182 @signed_terms_required
1183 @login_required
1184 def group_join(request, group_id):
1185     m = Membership(group_id=group_id,
1186                    person=request.user,
1187                    date_requested=datetime.now())
1188     try:
1189         m.save()
1190         post_save_redirect = reverse(
1191             'group_detail',
1192             kwargs=dict(group_id=group_id))
1193         return HttpResponseRedirect(post_save_redirect)
1194     except IntegrityError, e:
1195         logger.exception(e)
1196         msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1197         messages.error(request, msg)
1198         return group_search(request)
1199
1200
1201 @require_http_methods(["POST"])
1202 @signed_terms_required
1203 @login_required
1204 def group_leave(request, group_id):
1205     try:
1206         m = Membership.objects.select_related().get(
1207             group__id=group_id,
1208             person=request.user)
1209     except Membership.DoesNotExist:
1210         return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1211     if request.user in m.group.owner.all():
1212         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1213     return delete_object(
1214         request,
1215         model=Membership,
1216         object_id=m.id,
1217         template_name='im/astakosgroup_list.html',
1218         post_delete_redirect=reverse(
1219             'group_detail',
1220             kwargs=dict(group_id=group_id)))
1221
1222
1223 def handle_membership(func):
1224     @wraps(func)
1225     def wrapper(request, group_id, user_id):
1226         try:
1227             m = Membership.objects.select_related().get(
1228                 group__id=group_id,
1229                 person__id=user_id)
1230         except Membership.DoesNotExist:
1231             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1232         else:
1233             if request.user not in m.group.owner.all():
1234                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1235             func(request, m)
1236             return group_detail(request, group_id)
1237     return wrapper
1238
1239
1240 #@require_http_methods(["POST"])
1241 @require_http_methods(["POST", "GET"])
1242 @signed_terms_required
1243 @login_required
1244 @handle_membership
1245 def approve_member(request, membership):
1246     try:
1247         membership.approve()
1248         realname = membership.person.realname
1249         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1250         messages.success(request, msg)
1251     except AssertionError:
1252         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1253         messages.error(request, msg)
1254     except BaseException, e:
1255         logger.exception(e)
1256         realname = membership.person.realname
1257         msg = _(astakos_messages.GENERIC_ERROR)
1258         messages.error(request, msg)
1259
1260
1261 @signed_terms_required
1262 @login_required
1263 @handle_membership
1264 def disapprove_member(request, membership):
1265     try:
1266         membership.disapprove()
1267         realname = membership.person.realname
1268         msg = astakos_messages.MEMBER_REMOVED % locals()
1269         messages.success(request, msg)
1270     except BaseException, e:
1271         logger.exception(e)
1272         msg = _(astakos_messages.GENERIC_ERROR)
1273         messages.error(request, msg)
1274
1275
1276 #@require_http_methods(["GET"])
1277 @require_http_methods(["POST", "GET"])
1278 @signed_terms_required
1279 @login_required
1280 def resource_list(request):
1281     def with_class(entry):
1282         entry['load_class'] = 'red'
1283         max_value = float(entry['maxValue'])
1284         curr_value = float(entry['currValue'])
1285         if max_value > 0 :
1286             entry['ratio'] = (curr_value / max_value) * 100
1287         else:
1288             entry['ratio'] = 0
1289         if entry['ratio'] < 66:
1290             entry['load_class'] = 'yellow'
1291         if entry['ratio'] < 33:
1292             entry['load_class'] = 'green'
1293         return entry
1294
1295     def pluralize(entry):
1296         entry['plural'] = engine.plural(entry.get('name'))
1297         return entry
1298
1299     result = callpoint.get_user_status(request.user.id)
1300     if result.is_success:
1301         backenddata = map(with_class, result.data)
1302         data = map(pluralize, result.data)
1303     else:
1304         data = None
1305         messages.error(request, result.reason)
1306     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1307     resource_catalog.update_from_result_report(result)
1308
1309
1310
1311     return render_response('im/resource_list.html',
1312                            data=data,
1313                            context_instance=get_context(request),
1314                            resource_catalog=resource_catalog,
1315                            result=result)
1316
1317
1318 def group_create_list(request):
1319     form = PickResourceForm()
1320     return render_response(
1321         template='im/astakosgroup_create_list.html',
1322         context_instance=get_context(request),)
1323
1324
1325 ##@require_http_methods(["GET"])
1326 #@require_http_methods(["POST", "GET"])
1327 #@signed_terms_required
1328 #@login_required
1329 #def billing(request):
1330 #
1331 #    today = datetime.today()
1332 #    month_last_day = calendar.monthrange(today.year, today.month)[1]
1333 #    start = request.POST.get('datefrom', None)
1334 #    if start:
1335 #        today = datetime.fromtimestamp(int(start))
1336 #        month_last_day = calendar.monthrange(today.year, today.month)[1]
1337 #
1338 #    start = datetime(today.year, today.month, 1).strftime("%s")
1339 #    end = datetime(today.year, today.month, month_last_day).strftime("%s")
1340 #    r = request_billing.apply(args=('pgerakios@grnet.gr',
1341 #                                    int(start) * 1000,
1342 #                                    int(end) * 1000))
1343 #    data = {}
1344 #
1345 #    try:
1346 #        status, data = r.result
1347 #        data = _clear_billing_data(data)
1348 #        if status != 200:
1349 #            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1350 #    except:
1351 #        messages.error(request, r.result)
1352 #
1353 #    return render_response(
1354 #        template='im/billing.html',
1355 #        context_instance=get_context(request),
1356 #        data=data,
1357 #        zerodate=datetime(month=1, year=1970, day=1),
1358 #        today=today,
1359 #        start=int(start),
1360 #        month_last_day=month_last_day)
1361
1362
1363 #def _clear_billing_data(data):
1364 #
1365 #    # remove addcredits entries
1366 #    def isnotcredit(e):
1367 #        return e['serviceName'] != "addcredits"
1368 #
1369 #    # separate services
1370 #    def servicefilter(service_name):
1371 #        service = service_name
1372 #
1373 #        def fltr(e):
1374 #            return e['serviceName'] == service
1375 #        return fltr
1376 #
1377 #    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1378 #    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1379 #    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1380 #    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1381 #
1382 #    return data
1383
1384
1385 #@require_http_methods(["GET"])
1386 @require_http_methods(["POST", "GET"])
1387 @signed_terms_required
1388 @login_required
1389 def timeline(request):
1390 #    data = {'entity':request.user.email}
1391     timeline_body = ()
1392     timeline_header = ()
1393 #    form = TimelineForm(data)
1394     form = TimelineForm()
1395     if request.method == 'POST':
1396         data = request.POST
1397         form = TimelineForm(data)
1398         if form.is_valid():
1399             data = form.cleaned_data
1400             timeline_header = ('entity', 'resource',
1401                                'event name', 'event date',
1402                                'incremental cost', 'total cost')
1403             timeline_body = timeline_charge(
1404                 data['entity'], data['resource'],
1405                 data['start_date'], data['end_date'],
1406                 data['details'], data['operation'])
1407
1408     return render_response(template='im/timeline.html',
1409                            context_instance=get_context(request),
1410                            form=form,
1411                            timeline_header=timeline_header,
1412                            timeline_body=timeline_body)
1413     return data
1414
1415 # TODO: action only on POST and user should confirm the removal
1416 @require_http_methods(["GET", "POST"])
1417 @login_required
1418 @signed_terms_required
1419 def remove_auth_provider(request, pk):
1420     try:
1421         provider = request.user.auth_providers.get(pk=pk)
1422     except AstakosUserAuthProvider.DoesNotExist:
1423         raise Http404
1424
1425     if provider.can_remove():
1426         provider.delete()
1427         return HttpResponseRedirect(reverse('edit_profile'))
1428     else:
1429         raise PermissionDenied
1430