Merge branch 'devel-0.13' of https://code.grnet.gr/git/astakos into devel-0.13
[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
62 from django.template.loader import render_to_string
63 from django.views.decorators.http import require_http_methods
64 from astakos.im.activation_backends import get_backend, SimpleBackend
65
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         """
941     params = {'userid':request.user.id}
942
943     # validate sorting
944     sorting = 'groupname'
945     sort_form = AstakosGroupSortForm(request.GET)
946     if sort_form.is_valid():
947         sorting = sort_form.cleaned_data.get('sorting')
948     query = query+" ORDER BY %s ASC" %sorting
949     
950     q = AstakosGroup.objects.raw(query, params=params)
951     
952     # Create the template, context, response
953     template_name = "%s/%s_list.html" % (
954         q.model._meta.app_label,
955         q.model._meta.object_name.lower()
956     )
957     extra_context = dict(
958         is_search=False,
959         q=q,
960         sorting=sorting,
961         page=request.GET.get('page', 1)
962     )
963     return render_response(template_name,
964                            context_instance=get_context(request, extra_context)
965     )
966
967
968 @require_http_methods(["GET", "POST"])
969 @signed_terms_required
970 @login_required
971 def group_detail(request, group_id):
972     q = AstakosGroup.objects.select_related().filter(pk=group_id)
973     q = q.extra(select={
974         'is_member': """SELECT CASE WHEN EXISTS(
975                             SELECT id FROM im_membership
976                             WHERE group_id = im_astakosgroup.group_ptr_id
977                             AND person_id = %s)
978                         THEN 1 ELSE 0 END""" % request.user.id,
979         'is_owner': """SELECT CASE WHEN EXISTS(
980                         SELECT id FROM im_astakosuser_owner
981                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
982                         AND astakosuser_id = %s)
983                         THEN 1 ELSE 0 END""" % request.user.id,
984         'kindname': """SELECT name FROM im_groupkind
985                        WHERE id = im_astakosgroup.kind_id"""})
986
987     model = q.model
988     context_processors = None
989     mimetype = None
990     try:
991         obj = q.get()
992     except AstakosGroup.DoesNotExist:
993         raise Http404("No %s found matching the query" % (
994             model._meta.verbose_name))
995
996     update_form = AstakosGroupUpdateForm(instance=obj)
997     addmembers_form = AddGroupMembersForm()
998     if request.method == 'POST':
999         update_data = {}
1000         addmembers_data = {}
1001         for k, v in request.POST.iteritems():
1002             if k in update_form.fields:
1003                 update_data[k] = v
1004             if k in addmembers_form.fields:
1005                 addmembers_data[k] = v
1006         update_data = update_data or None
1007         addmembers_data = addmembers_data or None
1008         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1009         addmembers_form = AddGroupMembersForm(addmembers_data)
1010         if update_form.is_valid():
1011             update_form.save()
1012         if addmembers_form.is_valid():
1013             try:
1014                 map(obj.approve_member, addmembers_form.valid_users)
1015             except AssertionError:
1016                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1017                 messages.error(request, msg)
1018             addmembers_form = AddGroupMembersForm()
1019
1020     template_name = "%s/%s_detail.html" % (
1021         model._meta.app_label, model._meta.object_name.lower())
1022     t = template_loader.get_template(template_name)
1023     c = RequestContext(request, {
1024         'object': obj,
1025     }, context_processors)
1026
1027     # validate sorting
1028     sorting = 'person__email'
1029     form = MembersSortForm(request.GET)
1030     if form.is_valid():
1031         sorting = form.cleaned_data.get('sorting')
1032     
1033     result = callpoint.list_resources()
1034     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1035     resource_catalog.update_from_result(result)
1036
1037
1038     if not result.is_success:
1039         messages.error(
1040             request,
1041             'Unable to retrieve system resources: %s' % result.reason
1042     )
1043
1044     extra_context = {'update_form': update_form,
1045                      'addmembers_form': addmembers_form,
1046                      'page': request.GET.get('page', 1),
1047                      'sorting': sorting,
1048                      'resource_catalog':resource_catalog,
1049                      'quota':resource_catalog.get_quota(obj.quota)}
1050     for key, value in extra_context.items():
1051         if callable(value):
1052             c[key] = value()
1053         else:
1054             c[key] = value
1055     response = HttpResponse(t.render(c), mimetype=mimetype)
1056     populate_xheaders(
1057         request, response, model, getattr(obj, obj._meta.pk.name))
1058     return response
1059
1060
1061 @require_http_methods(["GET", "POST"])
1062 @signed_terms_required
1063 @login_required
1064 def group_search(request, extra_context=None, **kwargs):
1065     q = request.GET.get('q')
1066     if request.method == 'GET':
1067         form = AstakosGroupSearchForm({'q': q} if q else None)
1068     else:
1069         form = AstakosGroupSearchForm(get_query(request))
1070         if form.is_valid():
1071             q = form.cleaned_data['q'].strip()
1072     
1073     sorting = 'groupname'
1074     if q:
1075         queryset = AstakosGroup.objects.select_related()
1076         queryset = queryset.filter(name__contains=q)
1077         queryset = queryset.filter(approval_date__isnull=False)
1078         queryset = queryset.extra(select={
1079                                   'groupname': "auth_group.name",
1080                                   'kindname': "im_groupkind.name",
1081                                   'approved_members_num': """
1082                     SELECT COUNT(*) FROM im_membership
1083                     WHERE group_id = im_astakosgroup.group_ptr_id
1084                     AND date_joined IS NOT NULL""",
1085                                   'membership_approval_date': """
1086                     SELECT date_joined FROM im_membership
1087                     WHERE group_id = im_astakosgroup.group_ptr_id
1088                     AND person_id = %s""" % request.user.id,
1089                                   'is_member': """
1090                     SELECT CASE WHEN EXISTS(
1091                     SELECT date_joined FROM im_membership
1092                     WHERE group_id = im_astakosgroup.group_ptr_id
1093                     AND person_id = %s)
1094                     THEN 1 ELSE 0 END""" % request.user.id,
1095                                   'is_owner': """
1096                     SELECT CASE WHEN EXISTS(
1097                     SELECT id FROM im_astakosuser_owner
1098                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1099                     AND astakosuser_id = %s)
1100                     THEN 1 ELSE 0 END""" % request.user.id,
1101                     'is_owner': """SELECT CASE WHEN EXISTS(
1102                         SELECT id FROM im_astakosuser_owner
1103                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1104                         AND astakosuser_id = %s)
1105                         THEN 1 ELSE 0 END""" % request.user.id,
1106                     })
1107         
1108         # validate sorting
1109         sort_form = AstakosGroupSortForm(request.GET)
1110         if sort_form.is_valid():
1111             sorting = sort_form.cleaned_data.get('sorting')
1112         queryset = queryset.order_by(sorting)
1113
1114     else:
1115         queryset = AstakosGroup.objects.none()
1116     return object_list(
1117         request,
1118         queryset,
1119         paginate_by=PAGINATE_BY_ALL,
1120         page=request.GET.get('page') or 1,
1121         template_name='im/astakosgroup_list.html',
1122         extra_context=dict(form=form,
1123                            is_search=True,
1124                            q=q,
1125                            sorting=sorting))
1126
1127
1128 @require_http_methods(["GET", "POST"])
1129 @signed_terms_required
1130 @login_required
1131 def group_all(request, extra_context=None, **kwargs):
1132     q = AstakosGroup.objects.select_related()
1133     q = q.filter(approval_date__isnull=False)
1134     q = q.extra(select={
1135                 'groupname': "auth_group.name",
1136                 'kindname': "im_groupkind.name",
1137                 'approved_members_num': """
1138                     SELECT COUNT(*) FROM im_membership
1139                     WHERE group_id = im_astakosgroup.group_ptr_id
1140                     AND date_joined IS NOT NULL""",
1141                 'membership_approval_date': """
1142                     SELECT date_joined FROM im_membership
1143                     WHERE group_id = im_astakosgroup.group_ptr_id
1144                     AND person_id = %s""" % request.user.id,
1145                 'is_member': """
1146                     SELECT CASE WHEN EXISTS(
1147                     SELECT date_joined FROM im_membership
1148                     WHERE group_id = im_astakosgroup.group_ptr_id
1149                     AND person_id = %s)
1150                     THEN 1 ELSE 0 END""" % request.user.id,
1151                  'is_owner': """SELECT CASE WHEN EXISTS(
1152                         SELECT id FROM im_astakosuser_owner
1153                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1154                         AND astakosuser_id = %s)
1155                         THEN 1 ELSE 0 END""" % request.user.id,   })
1156     
1157     # validate sorting
1158     sorting = 'groupname'
1159     print '>>>', sorting, request.GET
1160     sort_form = AstakosGroupSortForm(request.GET)
1161     if sort_form.is_valid():
1162         sorting = sort_form.cleaned_data.get('sorting')
1163     print '<<<', sorting
1164     q = q.order_by(sorting)
1165     
1166     return object_list(
1167         request,
1168         q,
1169         paginate_by=PAGINATE_BY_ALL,
1170         page=request.GET.get('page') or 1,
1171         template_name='im/astakosgroup_list.html',
1172         extra_context=dict(form=AstakosGroupSearchForm(),
1173                            is_search=True,
1174                            sorting=sorting))
1175
1176
1177 #@require_http_methods(["POST"])
1178 @require_http_methods(["POST", "GET"])
1179 @signed_terms_required
1180 @login_required
1181 def group_join(request, group_id):
1182     m = Membership(group_id=group_id,
1183                    person=request.user,
1184                    date_requested=datetime.now())
1185     try:
1186         m.save()
1187         post_save_redirect = reverse(
1188             'group_detail',
1189             kwargs=dict(group_id=group_id))
1190         return HttpResponseRedirect(post_save_redirect)
1191     except IntegrityError, e:
1192         logger.exception(e)
1193         msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1194         messages.error(request, msg)
1195         return group_search(request)
1196
1197
1198 @require_http_methods(["POST"])
1199 @signed_terms_required
1200 @login_required
1201 def group_leave(request, group_id):
1202     try:
1203         m = Membership.objects.select_related().get(
1204             group__id=group_id,
1205             person=request.user)
1206     except Membership.DoesNotExist:
1207         return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1208     if request.user in m.group.owner.all():
1209         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1210     return delete_object(
1211         request,
1212         model=Membership,
1213         object_id=m.id,
1214         template_name='im/astakosgroup_list.html',
1215         post_delete_redirect=reverse(
1216             'group_detail',
1217             kwargs=dict(group_id=group_id)))
1218
1219
1220 def handle_membership(func):
1221     @wraps(func)
1222     def wrapper(request, group_id, user_id):
1223         try:
1224             m = Membership.objects.select_related().get(
1225                 group__id=group_id,
1226                 person__id=user_id)
1227         except Membership.DoesNotExist:
1228             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1229         else:
1230             if request.user not in m.group.owner.all():
1231                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1232             func(request, m)
1233             return group_detail(request, group_id)
1234     return wrapper
1235
1236
1237 #@require_http_methods(["POST"])
1238 @require_http_methods(["POST", "GET"])
1239 @signed_terms_required
1240 @login_required
1241 @handle_membership
1242 def approve_member(request, membership):
1243     try:
1244         membership.approve()
1245         realname = membership.person.realname
1246         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1247         messages.success(request, msg)
1248     except AssertionError:
1249         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1250         messages.error(request, msg)
1251     except BaseException, e:
1252         logger.exception(e)
1253         realname = membership.person.realname
1254         msg = _(astakos_messages.GENERIC_ERROR)
1255         messages.error(request, msg)
1256
1257
1258 @signed_terms_required
1259 @login_required
1260 @handle_membership
1261 def disapprove_member(request, membership):
1262     try:
1263         membership.disapprove()
1264         realname = membership.person.realname
1265         msg = astakos_messages.MEMBER_REMOVED % locals()
1266         messages.success(request, msg)
1267     except BaseException, e:
1268         logger.exception(e)
1269         msg = _(astakos_messages.GENERIC_ERROR)
1270         messages.error(request, msg)
1271
1272
1273 #@require_http_methods(["GET"])
1274 @require_http_methods(["POST", "GET"])
1275 @signed_terms_required
1276 @login_required
1277 def resource_list(request):
1278     def with_class(entry):
1279         entry['load_class'] = 'red'
1280         max_value = float(entry['maxValue'])
1281         curr_value = float(entry['currValue'])
1282         if max_value > 0 :
1283             entry['ratio'] = (curr_value / max_value) * 100
1284         else:
1285             entry['ratio'] = 0
1286         if entry['ratio'] < 66:
1287             entry['load_class'] = 'yellow'
1288         if entry['ratio'] < 33:
1289             entry['load_class'] = 'green'
1290         return entry
1291
1292     def pluralize(entry):
1293         entry['plural'] = engine.plural(entry.get('name'))
1294         return entry
1295
1296     result = callpoint.get_user_status(request.user.id)
1297     if result.is_success:
1298         backenddata = map(with_class, result.data)
1299         data = map(pluralize, result.data)
1300     else:
1301         data = None
1302         messages.error(request, result.reason)
1303     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1304     resource_catalog.update_from_result_report(result)
1305
1306
1307
1308     return render_response('im/resource_list.html',
1309                            data=data,
1310                            context_instance=get_context(request),
1311                            resource_catalog=resource_catalog,
1312                            result=result)
1313
1314
1315 def group_create_list(request):
1316     form = PickResourceForm()
1317     return render_response(
1318         template='im/astakosgroup_create_list.html',
1319         context_instance=get_context(request),)
1320
1321
1322 #@require_http_methods(["GET"])
1323 @require_http_methods(["POST", "GET"])
1324 @signed_terms_required
1325 @login_required
1326 def billing(request):
1327
1328     today = datetime.today()
1329     month_last_day = calendar.monthrange(today.year, today.month)[1]
1330     start = request.POST.get('datefrom', None)
1331     if start:
1332         today = datetime.fromtimestamp(int(start))
1333         month_last_day = calendar.monthrange(today.year, today.month)[1]
1334
1335     start = datetime(today.year, today.month, 1).strftime("%s")
1336     end = datetime(today.year, today.month, month_last_day).strftime("%s")
1337     r = request_billing.apply(args=('pgerakios@grnet.gr',
1338                                     int(start) * 1000,
1339                                     int(end) * 1000))
1340     data = {}
1341
1342     try:
1343         status, data = r.result
1344         data = _clear_billing_data(data)
1345         if status != 200:
1346             messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1347     except:
1348         messages.error(request, r.result)
1349
1350     return render_response(
1351         template='im/billing.html',
1352         context_instance=get_context(request),
1353         data=data,
1354         zerodate=datetime(month=1, year=1970, day=1),
1355         today=today,
1356         start=int(start),
1357         month_last_day=month_last_day)
1358
1359
1360 def _clear_billing_data(data):
1361
1362     # remove addcredits entries
1363     def isnotcredit(e):
1364         return e['serviceName'] != "addcredits"
1365
1366     # separate services
1367     def servicefilter(service_name):
1368         service = service_name
1369
1370         def fltr(e):
1371             return e['serviceName'] == service
1372         return fltr
1373
1374     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1375     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1376     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1377     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1378
1379     return data
1380
1381
1382 #@require_http_methods(["GET"])
1383 @require_http_methods(["POST", "GET"])
1384 @signed_terms_required
1385 @login_required
1386 def timeline(request):
1387 #    data = {'entity':request.user.email}
1388     timeline_body = ()
1389     timeline_header = ()
1390 #    form = TimelineForm(data)
1391     form = TimelineForm()
1392     if request.method == 'POST':
1393         data = request.POST
1394         form = TimelineForm(data)
1395         if form.is_valid():
1396             data = form.cleaned_data
1397             timeline_header = ('entity', 'resource',
1398                                'event name', 'event date',
1399                                'incremental cost', 'total cost')
1400             timeline_body = timeline_charge(
1401                 data['entity'], data['resource'],
1402                 data['start_date'], data['end_date'],
1403                 data['details'], data['operation'])
1404
1405     return render_response(template='im/timeline.html',
1406                            context_instance=get_context(request),
1407                            form=form,
1408                            timeline_header=timeline_header,
1409                            timeline_body=timeline_body)
1410     return data
1411
1412 # TODO: action only on POST and user should confirm the removal
1413 @require_http_methods(["GET", "POST"])
1414 @login_required
1415 @signed_terms_required
1416 def remove_auth_provider(request, pk):
1417     try:
1418         provider = request.user.auth_providers.get(pk=pk)
1419     except AstakosUserAuthProvider.DoesNotExist:
1420         raise Http404
1421
1422     if provider.can_remove():
1423         provider.delete()
1424         return HttpResponseRedirect(reverse('edit_profile'))
1425     else:
1426         raise PermissionDenied
1427