Validate sorting request parameter in group views
[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     try:
411         if not backend:
412             backend = get_backend(request)
413         form = backend.get_signup_form(provider, instance)
414     except Exception, e:
415         form = SimpleBackend(request).get_signup_form(provider)
416         messages.error(request, e)
417     if request.method == 'POST':
418         if form.is_valid():
419             user = form.save(commit=False)
420             try:
421                 result = backend.handle_activation(user)
422                 status = messages.SUCCESS
423                 message = result.message
424
425                 form.store_user(user, request)
426
427                 if 'additional_email' in form.cleaned_data:
428                     additional_email = form.cleaned_data['additional_email']
429                     if additional_email != user.email:
430                         user.additionalmail_set.create(email=additional_email)
431                         msg = 'Additional email: %s saved for user %s.' % (
432                             additional_email,
433                             user.email
434                         )
435                         logger._log(LOGGING_LEVEL, msg, [])
436                 if user and user.is_active:
437                     next = request.POST.get('next', '')
438                     response = prepare_response(request, user, next=next)
439                     transaction.commit()
440                     return response
441                 messages.add_message(request, status, message)
442                 transaction.commit()
443                 return render_response(
444                     on_success,
445                     context_instance=get_context(
446                         request,
447                         extra_context
448                     )
449                 )
450             except SendMailError, e:
451                 logger.exception(e)
452                 status = messages.ERROR
453                 message = e.message
454                 messages.error(request, message)
455                 transaction.rollback()
456             except BaseException, e:
457                 logger.exception(e)
458                 message = _(astakos_messages.GENERIC_ERROR)
459                 messages.error(request, message)
460                 logger.exception(e)
461                 transaction.rollback()
462     return render_response(template_name,
463                            signup_form=form,
464                            provider=provider,
465                            context_instance=get_context(request, extra_context))
466
467
468 @require_http_methods(["GET", "POST"])
469 @login_required
470 @signed_terms_required
471 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
472     """
473     Allows a user to send feedback.
474
475     In case of GET request renders a form for providing the feedback information.
476     In case of POST sends an email to support team.
477
478     If the user isn't logged in, redirects to settings.LOGIN_URL.
479
480     **Arguments**
481
482     ``template_name``
483         A custom template to use. This is optional; if not specified,
484         this will default to ``im/feedback.html``.
485
486     ``extra_context``
487         An dictionary of variables to add to the template context.
488
489     **Template:**
490
491     im/signup.html or ``template_name`` keyword argument.
492
493     **Settings:**
494
495     * LOGIN_URL: login uri
496     """
497     extra_context = extra_context or {}
498     if request.method == 'GET':
499         form = FeedbackForm()
500     if request.method == 'POST':
501         if not request.user:
502             return HttpResponse('Unauthorized', status=401)
503
504         form = FeedbackForm(request.POST)
505         if form.is_valid():
506             msg = form.cleaned_data['feedback_msg']
507             data = form.cleaned_data['feedback_data']
508             try:
509                 send_feedback(msg, data, request.user, email_template_name)
510             except SendMailError, e:
511                 messages.error(request, message)
512             else:
513                 message = _(astakos_messages.FEEDBACK_SENT)
514                 messages.success(request, message)
515     return render_response(template_name,
516                            feedback_form=form,
517                            context_instance=get_context(request, extra_context))
518
519
520 @require_http_methods(["GET"])
521 @signed_terms_required
522 def logout(request, template='registration/logged_out.html', extra_context=None):
523     """
524     Wraps `django.contrib.auth.logout`.
525     """
526     extra_context = extra_context or {}
527     response = HttpResponse()
528     if request.user.is_authenticated():
529         email = request.user.email
530         auth_logout(request)
531     next = restrict_next(
532         request.GET.get('next'),
533         domain=COOKIE_DOMAIN
534     )
535     if next:
536         response['Location'] = next
537         response.status_code = 302
538     elif LOGOUT_NEXT:
539         response['Location'] = LOGOUT_NEXT
540         response.status_code = 301
541     else:
542         messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
543         context = get_context(request, extra_context)
544         response.write(render_to_string(template, context_instance=context))
545     return response
546
547
548 @require_http_methods(["GET", "POST"])
549 @transaction.commit_manually
550 def activate(request, greeting_email_template_name='im/welcome_email.txt',
551              helpdesk_email_template_name='im/helpdesk_notification.txt'):
552     """
553     Activates the user identified by the ``auth`` request parameter, sends a welcome email
554     and renews the user token.
555
556     The view uses commit_manually decorator in order to ensure the user state will be updated
557     only if the email will be send successfully.
558     """
559     token = request.GET.get('auth')
560     next = request.GET.get('next')
561     try:
562         user = AstakosUser.objects.get(auth_token=token)
563     except AstakosUser.DoesNotExist:
564         return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
565
566     if user.is_active:
567         message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
568         messages.error(request, message)
569         return index(request)
570
571     try:
572         activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
573         response = prepare_response(request, user, next, renew=True)
574         transaction.commit()
575         return response
576     except SendMailError, e:
577         message = e.message
578         messages.add_message(request, messages.ERROR, message)
579         transaction.rollback()
580         return index(request)
581     except BaseException, e:
582         status = messages.ERROR
583         message = _(astakos_messages.GENERIC_ERROR)
584         messages.add_message(request, messages.ERROR, message)
585         logger.exception(e)
586         transaction.rollback()
587         return index(request)
588
589
590 @require_http_methods(["GET", "POST"])
591 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
592     extra_context = extra_context or {}
593     term = None
594     terms = None
595     if not term_id:
596         try:
597             term = ApprovalTerms.objects.order_by('-id')[0]
598         except IndexError:
599             pass
600     else:
601         try:
602             term = ApprovalTerms.objects.get(id=term_id)
603         except ApprovalTerms.DoesNotExist, e:
604             pass
605
606     if not term:
607         messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
608         return HttpResponseRedirect(reverse('index'))
609     f = open(term.location, 'r')
610     terms = f.read()
611
612     if request.method == 'POST':
613         next = restrict_next(
614             request.POST.get('next'),
615             domain=COOKIE_DOMAIN
616         )
617         if not next:
618             next = reverse('index')
619         form = SignApprovalTermsForm(request.POST, instance=request.user)
620         if not form.is_valid():
621             return render_response(template_name,
622                                    terms=terms,
623                                    approval_terms_form=form,
624                                    context_instance=get_context(request, extra_context))
625         user = form.save()
626         return HttpResponseRedirect(next)
627     else:
628         form = None
629         if request.user.is_authenticated() and not request.user.signed_terms:
630             form = SignApprovalTermsForm(instance=request.user)
631         return render_response(template_name,
632                                terms=terms,
633                                approval_terms_form=form,
634                                context_instance=get_context(request, extra_context))
635
636
637 @require_http_methods(["GET", "POST"])
638 @login_required
639 @signed_terms_required
640 @transaction.commit_manually
641 def change_email(request, activation_key=None,
642                  email_template_name='registration/email_change_email.txt',
643                  form_template_name='registration/email_change_form.html',
644                  confirm_template_name='registration/email_change_done.html',
645                  extra_context=None):
646     extra_context = extra_context or {}
647     if activation_key:
648         try:
649             user = EmailChange.objects.change_email(activation_key)
650             if request.user.is_authenticated() and request.user == user:
651                 msg = _(astakos_messages.EMAIL_CHANGED)
652                 messages.success(request, msg)
653                 auth_logout(request)
654                 response = prepare_response(request, user)
655                 transaction.commit()
656                 return response
657         except ValueError, e:
658             messages.error(request, e)
659         return render_response(confirm_template_name,
660                                modified_user=user if 'user' in locals(
661                                ) else None,
662                                context_instance=get_context(request,
663                                                             extra_context))
664
665     if not request.user.is_authenticated():
666         path = quote(request.get_full_path())
667         url = request.build_absolute_uri(reverse('index'))
668         return HttpResponseRedirect(url + '?next=' + path)
669     form = EmailChangeForm(request.POST or None)
670     if request.method == 'POST' and form.is_valid():
671         try:
672             ec = form.save(email_template_name, request)
673         except SendMailError, e:
674             msg = e
675             messages.error(request, msg)
676             transaction.rollback()
677         except IntegrityError, e:
678             msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
679             messages.error(request, msg)
680         else:
681             msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
682             messages.success(request, msg)
683             transaction.commit()
684     return render_response(
685         form_template_name,
686         form=form,
687         context_instance=get_context(request, extra_context)
688     )
689
690
691 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
692
693     if settings.MODERATION_ENABLED:
694         raise PermissionDenied
695
696     extra_context = extra_context or {}
697     try:
698         u = AstakosUser.objects.get(id=user_id)
699     except AstakosUser.DoesNotExist:
700         messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
701     else:
702         try:
703             send_activation_func(u)
704             msg = _(astakos_messages.ACTIVATION_SENT)
705             messages.success(request, msg)
706         except SendMailError, e:
707             messages.error(request, e)
708     return render_response(
709         template_name,
710         login_form = LoginForm(request=request),
711         context_instance = get_context(
712             request,
713             extra_context
714         )
715     )
716
717 class ResourcePresentation():
718
719     def __init__(self, data):
720         self.data = data
721
722     def update_from_result(self, result):
723         if result.is_success:
724             for r in result.data:
725                 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
726                 if not rname in self.data['resources']:
727                     self.data['resources'][rname] = {}
728
729                 self.data['resources'][rname].update(r)
730                 self.data['resources'][rname]['id'] = rname
731                 group = r.get('group')
732                 if not group in self.data['groups']:
733                     self.data['groups'][group] = {}
734
735                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
736
737     def test(self, quota_dict):
738         for k, v in quota_dict.iteritems():
739             rname = k
740             value = v
741             if not rname in self.data['resources']:
742                 self.data['resources'][rname] = {}
743
744
745             self.data['resources'][rname]['value'] = value
746
747
748     def update_from_result_report(self, result):
749         if result.is_success:
750             for r in result.data:
751                 rname = r.get('name')
752                 if not rname in self.data['resources']:
753                     self.data['resources'][rname] = {}
754
755                 self.data['resources'][rname].update(r)
756                 self.data['resources'][rname]['id'] = rname
757                 group = r.get('group')
758                 if not group in self.data['groups']:
759                     self.data['groups'][group] = {}
760
761                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
762
763     def get_group_resources(self, group):
764         return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
765
766     def get_groups_resources(self):
767         for g in self.data['groups']:
768             yield g, self.get_group_resources(g)
769
770     def get_quota(self, group_quotas):
771         for r, v in group_quotas.iteritems():
772             rname = str(r)
773             quota = self.data['resources'].get(rname)
774             quota['value'] = v
775             yield quota
776
777
778     def get_policies(self, policies_data):
779         for policy in policies_data:
780             rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
781             policy.update(self.data['resources'].get(rname))
782             yield policy
783
784     def __repr__(self):
785         return self.data.__repr__()
786
787     def __iter__(self, *args, **kwargs):
788         return self.data.__iter__(*args, **kwargs)
789
790     def __getitem__(self, *args, **kwargs):
791         return self.data.__getitem__(*args, **kwargs)
792
793     def get(self, *args, **kwargs):
794         return self.data.get(*args, **kwargs)
795
796
797
798 @require_http_methods(["GET", "POST"])
799 @signed_terms_required
800 @login_required
801 def group_add(request, kind_name='default'):
802
803     result = callpoint.list_resources()
804     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
805     resource_catalog.update_from_result(result)
806
807     if not result.is_success:
808         messages.error(
809             request,
810             'Unable to retrieve system resources: %s' % result.reason
811     )
812
813     try:
814         kind = GroupKind.objects.get(name=kind_name)
815     except:
816         return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
817
818
819
820     post_save_redirect = '/im/group/%(id)s/'
821     context_processors = None
822     model, form_class = get_model_and_form_class(
823         model=None,
824         form_class=AstakosGroupCreationForm
825     )
826
827     if request.method == 'POST':
828         form = form_class(request.POST, request.FILES)
829         if form.is_valid():
830             return render_response(
831                 template='im/astakosgroup_form_summary.html',
832                 context_instance=get_context(request),
833                 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
834                 policies = resource_catalog.get_policies(form.policies()),
835                 resource_catalog= resource_catalog,
836             )
837
838     else:
839         now = datetime.now()
840         data = {
841             'kind': kind,
842         }
843         for group, resources in resource_catalog.get_groups_resources():
844             data['is_selected_%s' % group] = False
845             for resource in resources:
846                 data['%s_uplimit' % resource] = ''
847
848         form = form_class(data)
849
850     # Create the template, context, response
851     template_name = "%s/%s_form.html" % (
852         model._meta.app_label,
853         model._meta.object_name.lower()
854     )
855     t = template_loader.get_template(template_name)
856     c = RequestContext(request, {
857         'form': form,
858         'kind': kind,
859         'resource_catalog':resource_catalog,
860     }, context_processors)
861     return HttpResponse(t.render(c))
862
863
864 #@require_http_methods(["POST"])
865 @require_http_methods(["GET", "POST"])
866 @signed_terms_required
867 @login_required
868 def group_add_complete(request):
869     model = AstakosGroup
870     form = AstakosGroupCreationSummaryForm(request.POST)
871     if form.is_valid():
872         d = form.cleaned_data
873         d['owners'] = [request.user]
874         result = callpoint.create_groups((d,)).next()
875         if result.is_success:
876             new_object = result.data[0]
877             msg = _(astakos_messages.OBJECT_CREATED) %\
878                 {"verbose_name": model._meta.verbose_name}
879             messages.success(request, msg, fail_silently=True)
880
881             # send notification
882             try:
883                 send_group_creation_notification(
884                     template_name='im/group_creation_notification.txt',
885                     dictionary={
886                         'group': new_object,
887                         'owner': request.user,
888                         'policies': d.get('policies', [])
889                     }
890                 )
891             except SendNotificationError, e:
892                 messages.error(request, e, fail_silently=True)
893             post_save_redirect = '/im/group/%(id)s/'
894             return HttpResponseRedirect(post_save_redirect % new_object)
895         else:
896             d = {"verbose_name": model._meta.verbose_name,
897                  "reason":result.reason}
898             msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
899             messages.error(request, msg, fail_silently=True)
900     return render_response(
901         template='im/astakosgroup_form_summary.html',
902         context_instance=get_context(request),
903         form=form)
904
905
906 #@require_http_methods(["GET"])
907 @require_http_methods(["GET", "POST"])
908 @signed_terms_required
909 @login_required
910 def group_list(request):
911     none = request.user.astakos_groups.none()
912     query = """
913         SELECT auth_group.id,
914         auth_group.name AS groupname,
915         im_groupkind.name AS kindname,
916         im_astakosgroup.*,
917         owner.email AS groupowner,
918         (SELECT COUNT(*) FROM im_membership
919             WHERE group_id = im_astakosgroup.group_ptr_id
920             AND date_joined IS NOT NULL) AS approved_members_num,
921         (SELECT CASE WHEN(
922                     SELECT date_joined FROM im_membership
923                     WHERE group_id = im_astakosgroup.group_ptr_id
924                     AND person_id = %(userid)s) IS NULL
925                     THEN 0 ELSE 1 END) AS membership_status
926         FROM im_astakosgroup
927         INNER JOIN im_membership ON (
928             im_astakosgroup.group_ptr_id = im_membership.group_id)
929         INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
930         INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
931         LEFT JOIN im_astakosuser_owner ON (
932             im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
933         LEFT JOIN auth_user as owner ON (
934             im_astakosuser_owner.astakosuser_id = owner.id)
935         WHERE im_membership.person_id = %(userid)s
936         """
937     params = {'userid':request.user.id}
938
939     # validate sorting
940     sorting = 'groupname'
941     sort_form = AstakosGroupSortForm(request.GET)
942     if sort_form.is_valid():
943         sorting = sort_form.cleaned_data.get('sorting')
944     query = query+" ORDER BY %s ASC" %sorting
945     
946     q = AstakosGroup.objects.raw(query, params=params)
947     
948     # Create the template, context, response
949     template_name = "%s/%s_list.html" % (
950         q.model._meta.app_label,
951         q.model._meta.object_name.lower()
952     )
953     extra_context = dict(
954         is_search=False,
955         q=q,
956         sorting=sorting,
957         page=request.GET.get('page', 1)
958     )
959     return render_response(template_name,
960                            context_instance=get_context(request, extra_context)
961     )
962
963
964 @require_http_methods(["GET", "POST"])
965 @signed_terms_required
966 @login_required
967 def group_detail(request, group_id):
968     q = AstakosGroup.objects.select_related().filter(pk=group_id)
969     q = q.extra(select={
970         'is_member': """SELECT CASE WHEN EXISTS(
971                             SELECT id FROM im_membership
972                             WHERE group_id = im_astakosgroup.group_ptr_id
973                             AND person_id = %s)
974                         THEN 1 ELSE 0 END""" % request.user.id,
975         'is_owner': """SELECT CASE WHEN EXISTS(
976                         SELECT id FROM im_astakosuser_owner
977                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
978                         AND astakosuser_id = %s)
979                         THEN 1 ELSE 0 END""" % request.user.id,
980         'kindname': """SELECT name FROM im_groupkind
981                        WHERE id = im_astakosgroup.kind_id"""})
982
983     model = q.model
984     context_processors = None
985     mimetype = None
986     try:
987         obj = q.get()
988     except AstakosGroup.DoesNotExist:
989         raise Http404("No %s found matching the query" % (
990             model._meta.verbose_name))
991
992     update_form = AstakosGroupUpdateForm(instance=obj)
993     addmembers_form = AddGroupMembersForm()
994     if request.method == 'POST':
995         update_data = {}
996         addmembers_data = {}
997         for k, v in request.POST.iteritems():
998             if k in update_form.fields:
999                 update_data[k] = v
1000             if k in addmembers_form.fields:
1001                 addmembers_data[k] = v
1002         update_data = update_data or None
1003         addmembers_data = addmembers_data or None
1004         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1005         addmembers_form = AddGroupMembersForm(addmembers_data)
1006         if update_form.is_valid():
1007             update_form.save()
1008         if addmembers_form.is_valid():
1009             try:
1010                 map(obj.approve_member, addmembers_form.valid_users)
1011             except AssertionError:
1012                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1013                 messages.error(request, msg)
1014             addmembers_form = AddGroupMembersForm()
1015
1016     template_name = "%s/%s_detail.html" % (
1017         model._meta.app_label, model._meta.object_name.lower())
1018     t = template_loader.get_template(template_name)
1019     c = RequestContext(request, {
1020         'object': obj,
1021     }, context_processors)
1022
1023     # validate sorting
1024     sorting = request.GET.get('sorting')
1025     if sorting:
1026         form = MembersSortForm({'sort_by': sorting})
1027         if form.is_valid():
1028             sorting = form.cleaned_data.get('sort_by')
1029
1030     else:
1031         form = MembersSortForm({'sort_by': 'person_first_name'})
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 % realname
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