Additional multiple auth methods fixes and tests
[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,
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 DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
97                                      'https://', '')"""
98
99 callpoint = AstakosCallpoint()
100
101 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
102     """
103     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
104     keyword argument and returns an ``django.http.HttpResponse`` with the
105     specified ``status``.
106     """
107     if tab is None:
108         tab = template.partition('_')[0].partition('.html')[0]
109     kwargs.setdefault('tab', tab)
110     html = template_loader.render_to_string(
111         template, kwargs, context_instance=context_instance)
112     response = HttpResponse(html, status=status)
113     return response
114
115 def requires_auth_provider(provider_id, **perms):
116     """
117     """
118     def decorator(func, *args, **kwargs):
119         @wraps(func)
120         def wrapper(request, *args, **kwargs):
121             provider = auth_providers.get_provider(provider_id)
122
123             if not provider or not provider.is_active():
124                 raise PermissionDenied
125
126             if provider:
127                 for pkey, value in perms.iteritems():
128                     attr = 'is_available_for_%s' % pkey.lower()
129                     if getattr(provider, attr)() != value:
130                         raise PermissionDenied
131             return func(request, *args)
132         return wrapper
133     return decorator
134
135
136 def requires_anonymous(func):
137     """
138     Decorator checkes whether the request.user is not Anonymous and in that case
139     redirects to `logout`.
140     """
141     @wraps(func)
142     def wrapper(request, *args):
143         if not request.user.is_anonymous():
144             next = urlencode({'next': request.build_absolute_uri()})
145             logout_uri = reverse(logout) + '?' + next
146             return HttpResponseRedirect(logout_uri)
147         return func(request, *args)
148     return wrapper
149
150
151 def signed_terms_required(func):
152     """
153     Decorator checkes whether the request.user is Anonymous and in that case
154     redirects to `logout`.
155     """
156     @wraps(func)
157     def wrapper(request, *args, **kwargs):
158         if request.user.is_authenticated() and not request.user.signed_terms:
159             params = urlencode({'next': request.build_absolute_uri(),
160                                 'show_form': ''})
161             terms_uri = reverse('latest_terms') + '?' + params
162             return HttpResponseRedirect(terms_uri)
163         return func(request, *args, **kwargs)
164     return wrapper
165
166
167 @require_http_methods(["GET", "POST"])
168 @signed_terms_required
169 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
170     """
171     If there is logged on user renders the profile page otherwise renders login page.
172
173     **Arguments**
174
175     ``login_template_name``
176         A custom login template to use. This is optional; if not specified,
177         this will default to ``im/login.html``.
178
179     ``profile_template_name``
180         A custom profile template to use. This is optional; if not specified,
181         this will default to ``im/profile.html``.
182
183     ``extra_context``
184         An dictionary of variables to add to the template context.
185
186     **Template:**
187
188     im/profile.html or im/login.html or ``template_name`` keyword argument.
189
190     """
191     extra_context = extra_context or {}
192     template_name = login_template_name
193     if request.user.is_authenticated():
194         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
195
196     return render_response(
197         template_name,
198         login_form = LoginForm(request=request),
199         context_instance = get_context(request, extra_context)
200     )
201
202
203 @require_http_methods(["GET", "POST"])
204 @login_required
205 @signed_terms_required
206 @transaction.commit_manually
207 def invite(request, template_name='im/invitations.html', extra_context=None):
208     """
209     Allows a user to invite somebody else.
210
211     In case of GET request renders a form for providing the invitee information.
212     In case of POST checks whether the user has not run out of invitations and then
213     sends an invitation email to singup to the service.
214
215     The view uses commit_manually decorator in order to ensure the number of the
216     user invitations is going to be updated only if the email has been successfully sent.
217
218     If the user isn't logged in, redirects to settings.LOGIN_URL.
219
220     **Arguments**
221
222     ``template_name``
223         A custom template to use. This is optional; if not specified,
224         this will default to ``im/invitations.html``.
225
226     ``extra_context``
227         An dictionary of variables to add to the template context.
228
229     **Template:**
230
231     im/invitations.html or ``template_name`` keyword argument.
232
233     **Settings:**
234
235     The view expectes the following settings are defined:
236
237     * LOGIN_URL: login uri
238     """
239     extra_context = extra_context or {}
240     status = None
241     message = None
242     form = InvitationForm()
243
244     inviter = request.user
245     if request.method == 'POST':
246         form = InvitationForm(request.POST)
247         if inviter.invitations > 0:
248             if form.is_valid():
249                 try:
250                     email = form.cleaned_data.get('username')
251                     realname = form.cleaned_data.get('realname')
252                     inviter.invite(email, realname)
253                     message = _(astakos_messages.INVITATION_SENT) % locals()
254                     messages.success(request, message)
255                 except SendMailError, e:
256                     message = e.message
257                     messages.error(request, message)
258                     transaction.rollback()
259                 except BaseException, e:
260                     message = _(astakos_messages.GENERIC_ERROR)
261                     messages.error(request, message)
262                     logger.exception(e)
263                     transaction.rollback()
264                 else:
265                     transaction.commit()
266         else:
267             message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
268             messages.error(request, message)
269
270     sent = [{'email': inv.username,
271              'realname': inv.realname,
272              'is_consumed': inv.is_consumed}
273             for inv in request.user.invitations_sent.all()]
274     kwargs = {'inviter': inviter,
275               'sent': sent}
276     context = get_context(request, extra_context, **kwargs)
277     return render_response(template_name,
278                            invitation_form=form,
279                            context_instance=context)
280
281
282 @require_http_methods(["GET", "POST"])
283 @login_required
284 @signed_terms_required
285 def edit_profile(request, template_name='im/profile.html', extra_context=None):
286     """
287     Allows a user to edit his/her profile.
288
289     In case of GET request renders a form for displaying the user information.
290     In case of POST updates the user informantion and redirects to ``next``
291     url parameter if exists.
292
293     If the user isn't logged in, redirects to settings.LOGIN_URL.
294
295     **Arguments**
296
297     ``template_name``
298         A custom template to use. This is optional; if not specified,
299         this will default to ``im/profile.html``.
300
301     ``extra_context``
302         An dictionary of variables to add to the template context.
303
304     **Template:**
305
306     im/profile.html or ``template_name`` keyword argument.
307
308     **Settings:**
309
310     The view expectes the following settings are defined:
311
312     * LOGIN_URL: login uri
313     """
314     extra_context = extra_context or {}
315     form = ProfileForm(
316         instance=request.user,
317         session_key=request.session.session_key
318     )
319     extra_context['next'] = request.GET.get('next')
320     if request.method == 'POST':
321         form = ProfileForm(
322             request.POST,
323             instance=request.user,
324             session_key=request.session.session_key
325         )
326         if form.is_valid():
327             try:
328                 prev_token = request.user.auth_token
329                 user = form.save()
330                 form = ProfileForm(
331                     instance=user,
332                     session_key=request.session.session_key
333                 )
334                 next = restrict_next(
335                     request.POST.get('next'),
336                     domain=COOKIE_DOMAIN
337                 )
338                 if next:
339                     return redirect(next)
340                 msg = _(astakos_messages.PROFILE_UPDATED)
341                 messages.success(request, msg)
342             except ValueError, ve:
343                 messages.success(request, ve)
344     elif request.method == "GET":
345         request.user.is_verified = True
346         request.user.save()
347
348     # existing providers
349     user_providers = request.user.get_active_auth_providers()
350
351     # providers that user can add
352     user_available_providers = request.user.get_available_auth_providers()
353
354     return render_response(template_name,
355                            profile_form = form,
356                            user_providers = user_providers,
357                            user_available_providers = user_available_providers,
358                            context_instance = get_context(request,
359                                                           extra_context))
360
361
362 @transaction.commit_manually
363 @require_http_methods(["GET", "POST"])
364 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
365     """
366     Allows a user to create a local account.
367
368     In case of GET request renders a form for entering the user information.
369     In case of POST handles the signup.
370
371     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
372     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
373     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
374     (see activation_backends);
375
376     Upon successful user creation, if ``next`` url parameter is present the user is redirected there
377     otherwise renders the same page with a success message.
378
379     On unsuccessful creation, renders ``template_name`` with an error message.
380
381     **Arguments**
382
383     ``template_name``
384         A custom template to render. This is optional;
385         if not specified, this will default to ``im/signup.html``.
386
387     ``on_success``
388         A custom template to render in case of success. This is optional;
389         if not specified, this will default to ``im/signup_complete.html``.
390
391     ``extra_context``
392         An dictionary of variables to add to the template context.
393
394     **Template:**
395
396     im/signup.html or ``template_name`` keyword argument.
397     im/signup_complete.html or ``on_success`` keyword argument.
398     """
399     extra_context = extra_context or {}
400     if request.user.is_authenticated():
401         return HttpResponseRedirect(reverse('edit_profile'))
402
403     provider = get_query(request).get('provider', 'local')
404     if not auth_providers.get_provider(provider).is_available_for_create():
405         raise PermissionDenied
406
407     id = get_query(request).get('id')
408     try:
409         instance = AstakosUser.objects.get(id=id) if id else None
410     except AstakosUser.DoesNotExist:
411         instance = None
412
413     try:
414         if not backend:
415             backend = get_backend(request)
416         form = backend.get_signup_form(provider, instance)
417     except Exception, e:
418         form = SimpleBackend(request).get_signup_form(provider)
419         messages.error(request, e)
420     if request.method == 'POST':
421         if form.is_valid():
422             user = form.save(commit=False)
423             try:
424                 result = backend.handle_activation(user)
425                 status = messages.SUCCESS
426                 message = result.message
427
428                 form.store_user(user, request)
429
430                 if 'additional_email' in form.cleaned_data:
431                     additional_email = form.cleaned_data['additional_email']
432                     if additional_email != user.email:
433                         user.additionalmail_set.create(email=additional_email)
434                         msg = 'Additional email: %s saved for user %s.' % (
435                             additional_email,
436                             user.email
437                         )
438                         logger._log(LOGGING_LEVEL, msg, [])
439                 if user and user.is_active:
440                     next = request.POST.get('next', '')
441                     response = prepare_response(request, user, next=next)
442                     transaction.commit()
443                     return response
444                 messages.add_message(request, status, message)
445                 transaction.commit()
446                 return render_response(
447                     on_success,
448                     context_instance=get_context(
449                         request,
450                         extra_context
451                     )
452                 )
453             except SendMailError, e:
454                 logger.exception(e)
455                 status = messages.ERROR
456                 message = e.message
457                 messages.error(request, message)
458                 transaction.rollback()
459             except BaseException, e:
460                 logger.exception(e)
461                 message = _(astakos_messages.GENERIC_ERROR)
462                 messages.error(request, message)
463                 logger.exception(e)
464                 transaction.rollback()
465     return render_response(template_name,
466                            signup_form=form,
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             return render_response(
834                 template='im/astakosgroup_form_summary.html',
835                 context_instance=get_context(request),
836                 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
837                 policies = resource_catalog.get_policies(form.policies()),
838                 resource_catalog= resource_catalog,
839             )
840
841     else:
842         now = datetime.now()
843         data = {
844             'kind': kind,
845         }
846         for group, resources in resource_catalog.get_groups_resources():
847             data['is_selected_%s' % group] = False
848             for resource in resources:
849                 data['%s_uplimit' % resource] = ''
850
851         form = form_class(data)
852
853     # Create the template, context, response
854     template_name = "%s/%s_form.html" % (
855         model._meta.app_label,
856         model._meta.object_name.lower()
857     )
858     t = template_loader.get_template(template_name)
859     c = RequestContext(request, {
860         'form': form,
861         'kind': kind,
862         'resource_catalog':resource_catalog,
863     }, context_processors)
864     return HttpResponse(t.render(c))
865
866
867 #@require_http_methods(["POST"])
868 @require_http_methods(["GET", "POST"])
869 @signed_terms_required
870 @login_required
871 def group_add_complete(request):
872     model = AstakosGroup
873     form = AstakosGroupCreationSummaryForm(request.POST)
874     if form.is_valid():
875         d = form.cleaned_data
876         d['owners'] = [request.user]
877         result = callpoint.create_groups((d,)).next()
878         if result.is_success:
879             new_object = result.data[0]
880             msg = _(astakos_messages.OBJECT_CREATED) %\
881                 {"verbose_name": model._meta.verbose_name}
882             messages.success(request, msg, fail_silently=True)
883
884             # send notification
885             try:
886                 send_group_creation_notification(
887                     template_name='im/group_creation_notification.txt',
888                     dictionary={
889                         'group': new_object,
890                         'owner': request.user,
891                         'policies': d.get('policies', [])
892                     }
893                 )
894             except SendNotificationError, e:
895                 messages.error(request, e, fail_silently=True)
896             post_save_redirect = '/im/group/%(id)s/'
897             return HttpResponseRedirect(post_save_redirect % new_object)
898         else:
899             d = {"verbose_name": model._meta.verbose_name,
900                  "reason":result.reason}
901             msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
902             messages.error(request, msg, fail_silently=True)
903     return render_response(
904         template='im/astakosgroup_form_summary.html',
905         context_instance=get_context(request),
906         form=form)
907
908
909 #@require_http_methods(["GET"])
910 @require_http_methods(["GET", "POST"])
911 @signed_terms_required
912 @login_required
913 def group_list(request):
914     none = request.user.astakos_groups.none()
915     sorting = request.GET.get('sorting')
916     query = """
917         SELECT auth_group.id,
918         %s 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 = %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 = %s
940         """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)
941
942     if sorting:
943         query = query+" ORDER BY %s ASC" %sorting
944     else:
945         query = query+" ORDER BY groupname ASC"
946     q = AstakosGroup.objects.raw(query)
947
948
949
950     # Create the template, context, response
951     template_name = "%s/%s_list.html" % (
952         q.model._meta.app_label,
953         q.model._meta.object_name.lower()
954     )
955     extra_context = dict(
956         is_search=False,
957         q=q,
958         sorting=request.GET.get('sorting'),
959         page=request.GET.get('page', 1)
960     )
961     return render_response(template_name,
962                            context_instance=get_context(request, extra_context)
963     )
964
965
966 @require_http_methods(["GET", "POST"])
967 @signed_terms_required
968 @login_required
969 def group_detail(request, group_id):
970     q = AstakosGroup.objects.select_related().filter(pk=group_id)
971     q = q.extra(select={
972         'is_member': """SELECT CASE WHEN EXISTS(
973                             SELECT id FROM im_membership
974                             WHERE group_id = im_astakosgroup.group_ptr_id
975                             AND person_id = %s)
976                         THEN 1 ELSE 0 END""" % request.user.id,
977         'is_owner': """SELECT CASE WHEN EXISTS(
978                         SELECT id FROM im_astakosuser_owner
979                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
980                         AND astakosuser_id = %s)
981                         THEN 1 ELSE 0 END""" % request.user.id,
982         'kindname': """SELECT name FROM im_groupkind
983                        WHERE id = im_astakosgroup.kind_id"""})
984
985     model = q.model
986     context_processors = None
987     mimetype = None
988     try:
989         obj = q.get()
990     except AstakosGroup.DoesNotExist:
991         raise Http404("No %s found matching the query" % (
992             model._meta.verbose_name))
993
994     update_form = AstakosGroupUpdateForm(instance=obj)
995     addmembers_form = AddGroupMembersForm()
996     if request.method == 'POST':
997         update_data = {}
998         addmembers_data = {}
999         for k, v in request.POST.iteritems():
1000             if k in update_form.fields:
1001                 update_data[k] = v
1002             if k in addmembers_form.fields:
1003                 addmembers_data[k] = v
1004         update_data = update_data or None
1005         addmembers_data = addmembers_data or None
1006         update_form = AstakosGroupUpdateForm(update_data, instance=obj)
1007         addmembers_form = AddGroupMembersForm(addmembers_data)
1008         if update_form.is_valid():
1009             update_form.save()
1010         if addmembers_form.is_valid():
1011             try:
1012                 map(obj.approve_member, addmembers_form.valid_users)
1013             except AssertionError:
1014                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1015                 messages.error(request, msg)
1016             addmembers_form = AddGroupMembersForm()
1017
1018     template_name = "%s/%s_detail.html" % (
1019         model._meta.app_label, model._meta.object_name.lower())
1020     t = template_loader.get_template(template_name)
1021     c = RequestContext(request, {
1022         'object': obj,
1023     }, context_processors)
1024
1025     # validate sorting
1026     sorting = request.GET.get('sorting')
1027     if sorting:
1028         form = MembersSortForm({'sort_by': sorting})
1029         if form.is_valid():
1030             sorting = form.cleaned_data.get('sort_by')
1031
1032     else:
1033         form = MembersSortForm({'sort_by': 'person_first_name'})
1034
1035     result = callpoint.list_resources()
1036     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1037     resource_catalog.update_from_result(result)
1038
1039
1040     if not result.is_success:
1041         messages.error(
1042             request,
1043             'Unable to retrieve system resources: %s' % result.reason
1044     )
1045
1046     extra_context = {'update_form': update_form,
1047                      'addmembers_form': addmembers_form,
1048                      'page': request.GET.get('page', 1),
1049                      'sorting': sorting,
1050                      'resource_catalog':resource_catalog,
1051                      'quota':resource_catalog.get_quota(obj.quota)}
1052     for key, value in extra_context.items():
1053         if callable(value):
1054             c[key] = value()
1055         else:
1056             c[key] = value
1057     response = HttpResponse(t.render(c), mimetype=mimetype)
1058     populate_xheaders(
1059         request, response, model, getattr(obj, obj._meta.pk.name))
1060     return response
1061
1062
1063 @require_http_methods(["GET", "POST"])
1064 @signed_terms_required
1065 @login_required
1066 def group_search(request, extra_context=None, **kwargs):
1067     q = request.GET.get('q')
1068     sorting = request.GET.get('sorting')
1069     if request.method == 'GET':
1070         form = AstakosGroupSearchForm({'q': q} if q else None)
1071     else:
1072         form = AstakosGroupSearchForm(get_query(request))
1073         if form.is_valid():
1074             q = form.cleaned_data['q'].strip()
1075     if q:
1076         queryset = AstakosGroup.objects.select_related()
1077         queryset = queryset.filter(name__contains=q)
1078         queryset = queryset.filter(approval_date__isnull=False)
1079         queryset = queryset.extra(select={
1080                                   'groupname': DB_REPLACE_GROUP_SCHEME,
1081                                   'kindname': "im_groupkind.name",
1082                                   'approved_members_num': """
1083                     SELECT COUNT(*) FROM im_membership
1084                     WHERE group_id = im_astakosgroup.group_ptr_id
1085                     AND date_joined IS NOT NULL""",
1086                                   'membership_approval_date': """
1087                     SELECT date_joined FROM im_membership
1088                     WHERE group_id = im_astakosgroup.group_ptr_id
1089                     AND person_id = %s""" % request.user.id,
1090                                   'is_member': """
1091                     SELECT CASE WHEN EXISTS(
1092                     SELECT date_joined FROM im_membership
1093                     WHERE group_id = im_astakosgroup.group_ptr_id
1094                     AND person_id = %s)
1095                     THEN 1 ELSE 0 END""" % request.user.id,
1096                                   'is_owner': """
1097                     SELECT CASE WHEN EXISTS(
1098                     SELECT id FROM im_astakosuser_owner
1099                     WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1100                     AND astakosuser_id = %s)
1101                     THEN 1 ELSE 0 END""" % request.user.id,
1102                     'is_owner': """SELECT CASE WHEN EXISTS(
1103                         SELECT id FROM im_astakosuser_owner
1104                         WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
1105                         AND astakosuser_id = %s)
1106                         THEN 1 ELSE 0 END""" % request.user.id,
1107                     })
1108         if sorting:
1109             # TODO check sorting value
1110             queryset = queryset.order_by(sorting)
1111         else:
1112             queryset = queryset.order_by("groupname")
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': DB_REPLACE_GROUP_SCHEME,
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     sorting = request.GET.get('sorting')
1157     if sorting:
1158         # TODO check sorting value
1159         q = q.order_by(sorting)
1160     else:
1161         q = q.order_by("groupname")
1162
1163     return object_list(
1164         request,
1165         q,
1166         paginate_by=PAGINATE_BY_ALL,
1167         page=request.GET.get('page') or 1,
1168         template_name='im/astakosgroup_list.html',
1169         extra_context=dict(form=AstakosGroupSearchForm(),
1170                            is_search=True,
1171                            sorting=sorting))
1172
1173
1174 #@require_http_methods(["POST"])
1175 @require_http_methods(["POST", "GET"])
1176 @signed_terms_required
1177 @login_required
1178 def group_join(request, group_id):
1179     m = Membership(group_id=group_id,
1180                    person=request.user,
1181                    date_requested=datetime.now())
1182     try:
1183         m.save()
1184         post_save_redirect = reverse(
1185             'group_detail',
1186             kwargs=dict(group_id=group_id))
1187         return HttpResponseRedirect(post_save_redirect)
1188     except IntegrityError, e:
1189         logger.exception(e)
1190         msg = _(astakos_messages.GROUP_JOIN_FAILURE)
1191         messages.error(request, msg)
1192         return group_search(request)
1193
1194
1195 @require_http_methods(["POST"])
1196 @signed_terms_required
1197 @login_required
1198 def group_leave(request, group_id):
1199     try:
1200         m = Membership.objects.select_related().get(
1201             group__id=group_id,
1202             person=request.user)
1203     except Membership.DoesNotExist:
1204         return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1205     if request.user in m.group.owner.all():
1206         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
1207     return delete_object(
1208         request,
1209         model=Membership,
1210         object_id=m.id,
1211         template_name='im/astakosgroup_list.html',
1212         post_delete_redirect=reverse(
1213             'group_detail',
1214             kwargs=dict(group_id=group_id)))
1215
1216
1217 def handle_membership(func):
1218     @wraps(func)
1219     def wrapper(request, group_id, user_id):
1220         try:
1221             m = Membership.objects.select_related().get(
1222                 group__id=group_id,
1223                 person__id=user_id)
1224         except Membership.DoesNotExist:
1225             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
1226         else:
1227             if request.user not in m.group.owner.all():
1228                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
1229             func(request, m)
1230             return group_detail(request, group_id)
1231     return wrapper
1232
1233
1234 #@require_http_methods(["POST"])
1235 @require_http_methods(["POST", "GET"])
1236 @signed_terms_required
1237 @login_required
1238 @handle_membership
1239 def approve_member(request, membership):
1240     try:
1241         membership.approve()
1242         realname = membership.person.realname
1243         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
1244         messages.success(request, msg)
1245     except AssertionError:
1246         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
1247         messages.error(request, msg)
1248     except BaseException, e:
1249         logger.exception(e)
1250         realname = membership.person.realname
1251         msg = _(astakos_messages.GENERIC_ERROR)
1252         messages.error(request, msg)
1253
1254
1255 @signed_terms_required
1256 @login_required
1257 @handle_membership
1258 def disapprove_member(request, membership):
1259     try:
1260         membership.disapprove()
1261         realname = membership.person.realname
1262         msg = astakos_messages.MEMBER_REMOVED % realname
1263         messages.success(request, msg)
1264     except BaseException, e:
1265         logger.exception(e)
1266         msg = _(astakos_messages.GENERIC_ERROR)
1267         messages.error(request, msg)
1268
1269
1270 #@require_http_methods(["GET"])
1271 @require_http_methods(["POST", "GET"])
1272 @signed_terms_required
1273 @login_required
1274 def resource_list(request):
1275     def with_class(entry):
1276         entry['load_class'] = 'red'
1277         max_value = float(entry['maxValue'])
1278         curr_value = float(entry['currValue'])
1279         if max_value > 0 :
1280             entry['ratio'] = (curr_value / max_value) * 100
1281         else:
1282             entry['ratio'] = 0
1283         if entry['ratio'] < 66:
1284             entry['load_class'] = 'yellow'
1285         if entry['ratio'] < 33:
1286             entry['load_class'] = 'green'
1287         return entry
1288
1289     def pluralize(entry):
1290         entry['plural'] = engine.plural(entry.get('name'))
1291         return entry
1292
1293     result = callpoint.get_user_status(request.user.id)
1294     if result.is_success:
1295         backenddata = map(with_class, result.data)
1296         data = map(pluralize, result.data)
1297     else:
1298         data = None
1299         messages.error(request, result.reason)
1300     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1301     resource_catalog.update_from_result_report(result)
1302
1303
1304
1305     return render_response('im/resource_list.html',
1306                            data=data,
1307                            context_instance=get_context(request),
1308                            resource_catalog=resource_catalog,
1309                            result=result)
1310
1311
1312 def group_create_list(request):
1313     form = PickResourceForm()
1314     return render_response(
1315         template='im/astakosgroup_create_list.html',
1316         context_instance=get_context(request),)
1317
1318
1319 #@require_http_methods(["GET"])
1320 @require_http_methods(["POST", "GET"])
1321 @signed_terms_required
1322 @login_required
1323 def billing(request):
1324
1325     today = datetime.today()
1326     month_last_day = calendar.monthrange(today.year, today.month)[1]
1327     start = request.POST.get('datefrom', None)
1328     if start:
1329         today = datetime.fromtimestamp(int(start))
1330         month_last_day = calendar.monthrange(today.year, today.month)[1]
1331
1332     start = datetime(today.year, today.month, 1).strftime("%s")
1333     end = datetime(today.year, today.month, month_last_day).strftime("%s")
1334     r = request_billing.apply(args=('pgerakios@grnet.gr',
1335                                     int(start) * 1000,
1336                                     int(end) * 1000))
1337     data = {}
1338
1339     try:
1340         status, data = r.result
1341         data = _clear_billing_data(data)
1342         if status != 200:
1343             messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
1344     except:
1345         messages.error(request, r.result)
1346
1347     return render_response(
1348         template='im/billing.html',
1349         context_instance=get_context(request),
1350         data=data,
1351         zerodate=datetime(month=1, year=1970, day=1),
1352         today=today,
1353         start=int(start),
1354         month_last_day=month_last_day)
1355
1356
1357 def _clear_billing_data(data):
1358
1359     # remove addcredits entries
1360     def isnotcredit(e):
1361         return e['serviceName'] != "addcredits"
1362
1363     # separate services
1364     def servicefilter(service_name):
1365         service = service_name
1366
1367         def fltr(e):
1368             return e['serviceName'] == service
1369         return fltr
1370
1371     data['bill_nocredits'] = filter(isnotcredit, data['bill'])
1372     data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
1373     data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
1374     data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
1375
1376     return data
1377
1378
1379 #@require_http_methods(["GET"])
1380 @require_http_methods(["POST", "GET"])
1381 @signed_terms_required
1382 @login_required
1383 def timeline(request):
1384 #    data = {'entity':request.user.email}
1385     timeline_body = ()
1386     timeline_header = ()
1387 #    form = TimelineForm(data)
1388     form = TimelineForm()
1389     if request.method == 'POST':
1390         data = request.POST
1391         form = TimelineForm(data)
1392         if form.is_valid():
1393             data = form.cleaned_data
1394             timeline_header = ('entity', 'resource',
1395                                'event name', 'event date',
1396                                'incremental cost', 'total cost')
1397             timeline_body = timeline_charge(
1398                 data['entity'], data['resource'],
1399                 data['start_date'], data['end_date'],
1400                 data['details'], data['operation'])
1401
1402     return render_response(template='im/timeline.html',
1403                            context_instance=get_context(request),
1404                            form=form,
1405                            timeline_header=timeline_header,
1406                            timeline_body=timeline_body)
1407     return data
1408
1409 # TODO: action only on POST and user should confirm the removal
1410 @require_http_methods(["GET", "POST"])
1411 @login_required
1412 @signed_terms_required
1413 def remove_auth_provider(request, pk):
1414     try:
1415         provider = request.user.auth_providers.get(pk=pk)
1416     except AstakosUserAuthProvider.DoesNotExist:
1417         raise Http404
1418
1419     if provider.can_remove():
1420         provider.delete()
1421         return HttpResponseRedirect(reverse('edit_profile'))
1422     else:
1423         raise PermissionDenied
1424