1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
37 from smtplib import SMTPException
38 from urllib import quote
39 from functools import wraps
41 from django.contrib import messages
42 from django.contrib.auth.decorators import login_required
43 from django.contrib.auth.views import password_change
44 from django.core.exceptions import ValidationError
45 from django.core.mail import send_mail
46 from django.core.urlresolvers import reverse
47 from django.db import transaction
48 from django.db.models import Q
49 from django.db.utils import IntegrityError
50 from django.forms.fields import URLField
51 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
52 HttpResponseRedirect, HttpResponseBadRequest
53 from django.shortcuts import redirect
54 from django.template.loader import render_to_string
55 from django.utils.http import urlencode
56 from django.utils.translation import ugettext as _
57 from django.views.generic.create_update import *
58 from django.views.generic.list_detail import *
60 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms, AstakosGroup, Resource
61 from astakos.im.activation_backends import get_backend, SimpleBackend
62 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
63 from astakos.im.forms import *
64 from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
65 invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
66 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
68 logger = logging.getLogger(__name__)
70 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
72 Calls ``django.template.loader.render_to_string`` with an additional ``tab``
73 keyword argument and returns an ``django.http.HttpResponse`` with the
77 tab = template.partition('_')[0].partition('.html')[0]
78 kwargs.setdefault('tab', tab)
79 html = render_to_string(template, kwargs, context_instance=context_instance)
80 response = HttpResponse(html, status=status)
82 set_cookie(response, context_instance['request'].user)
86 def requires_anonymous(func):
88 Decorator checkes whether the request.user is not Anonymous and in that case
89 redirects to `logout`.
92 def wrapper(request, *args):
93 if not request.user.is_anonymous():
94 next = urlencode({'next': request.build_absolute_uri()})
95 logout_uri = reverse(logout) + '?' + next
96 return HttpResponseRedirect(logout_uri)
97 return func(request, *args)
100 def signed_terms_required(func):
102 Decorator checkes whether the request.user is Anonymous and in that case
103 redirects to `logout`.
106 def wrapper(request, *args, **kwargs):
107 if request.user.is_authenticated() and not request.user.signed_terms():
108 params = urlencode({'next': request.build_absolute_uri(),
110 terms_uri = reverse('latest_terms') + '?' + params
111 return HttpResponseRedirect(terms_uri)
112 return func(request, *args, **kwargs)
115 @signed_terms_required
116 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
118 If there is logged on user renders the profile page otherwise renders login page.
122 ``login_template_name``
123 A custom login template to use. This is optional; if not specified,
124 this will default to ``im/login.html``.
126 ``profile_template_name``
127 A custom profile template to use. This is optional; if not specified,
128 this will default to ``im/profile.html``.
131 An dictionary of variables to add to the template context.
135 im/profile.html or im/login.html or ``template_name`` keyword argument.
138 template_name = login_template_name
139 if request.user.is_authenticated():
140 return HttpResponseRedirect(reverse('edit_profile'))
141 return render_response(template_name,
142 login_form = LoginForm(request=request),
143 context_instance = get_context(request, extra_context))
146 @signed_terms_required
147 @transaction.commit_manually
148 def invite(request, template_name='im/invitations.html', extra_context={}):
150 Allows a user to invite somebody else.
152 In case of GET request renders a form for providing the invitee information.
153 In case of POST checks whether the user has not run out of invitations and then
154 sends an invitation email to singup to the service.
156 The view uses commit_manually decorator in order to ensure the number of the
157 user invitations is going to be updated only if the email has been successfully sent.
159 If the user isn't logged in, redirects to settings.LOGIN_URL.
164 A custom template to use. This is optional; if not specified,
165 this will default to ``im/invitations.html``.
168 An dictionary of variables to add to the template context.
172 im/invitations.html or ``template_name`` keyword argument.
176 The view expectes the following settings are defined:
178 * LOGIN_URL: login uri
179 * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
180 * ASTAKOS_DEFAULT_FROM_EMAIL: from email
184 form = InvitationForm()
186 inviter = request.user
187 if request.method == 'POST':
188 form = InvitationForm(request.POST)
189 if inviter.invitations > 0:
192 invitation = form.save()
193 invite_func(invitation, inviter)
194 message = _('Invitation sent to %s' % invitation.username)
195 messages.success(request, message)
196 except SendMailError, e:
198 messages.error(request, message)
199 transaction.rollback()
200 except BaseException, e:
201 message = _('Something went wrong.')
202 messages.error(request, message)
204 transaction.rollback()
208 message = _('No invitations left')
209 messages.error(request, message)
211 sent = [{'email': inv.username,
212 'realname': inv.realname,
213 'is_consumed': inv.is_consumed}
214 for inv in request.user.invitations_sent.all()]
215 kwargs = {'inviter': inviter,
217 context = get_context(request, extra_context, **kwargs)
218 return render_response(template_name,
219 invitation_form = form,
220 context_instance = context)
223 @signed_terms_required
224 def edit_profile(request, template_name='im/profile.html', extra_context={}):
226 Allows a user to edit his/her profile.
228 In case of GET request renders a form for displaying the user information.
229 In case of POST updates the user informantion and redirects to ``next``
230 url parameter if exists.
232 If the user isn't logged in, redirects to settings.LOGIN_URL.
237 A custom template to use. This is optional; if not specified,
238 this will default to ``im/profile.html``.
241 An dictionary of variables to add to the template context.
245 im/profile.html or ``template_name`` keyword argument.
249 The view expectes the following settings are defined:
251 * LOGIN_URL: login uri
253 form = ProfileForm(instance=request.user)
254 extra_context['next'] = request.GET.get('next')
256 if request.method == 'POST':
257 form = ProfileForm(request.POST, instance=request.user)
260 prev_token = request.user.auth_token
262 reset_cookie = user.auth_token != prev_token
263 form = ProfileForm(instance=user)
264 next = request.POST.get('next')
266 return redirect(next)
267 msg = _('Profile has been updated successfully')
268 messages.success(request, msg)
269 except ValueError, ve:
270 messages.success(request, ve)
271 elif request.method == "GET":
272 if not request.user.is_verified:
273 request.user.is_verified = True
275 return render_response(template_name,
276 reset_cookie = reset_cookie,
278 context_instance = get_context(request,
281 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
283 Allows a user to create a local account.
285 In case of GET request renders a form for entering the user information.
286 In case of POST handles the signup.
288 The user activation will be delegated to the backend specified by the ``backend`` keyword argument
289 if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
290 if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
291 (see activation_backends);
293 Upon successful user creation, if ``next`` url parameter is present the user is redirected there
294 otherwise renders the same page with a success message.
296 On unsuccessful creation, renders ``template_name`` with an error message.
301 A custom template to render. This is optional;
302 if not specified, this will default to ``im/signup.html``.
305 A custom template to render in case of success. This is optional;
306 if not specified, this will default to ``im/signup_complete.html``.
309 An dictionary of variables to add to the template context.
313 im/signup.html or ``template_name`` keyword argument.
314 im/signup_complete.html or ``on_success`` keyword argument.
316 if request.user.is_authenticated():
317 return HttpResponseRedirect(reverse('edit_profile'))
319 provider = get_query(request).get('provider', 'local')
322 backend = get_backend(request)
323 form = backend.get_signup_form(provider)
325 form = SimpleBackend(request).get_signup_form(provider)
326 messages.error(request, e)
327 if request.method == 'POST':
329 user = form.save(commit=False)
331 result = backend.handle_activation(user)
332 status = messages.SUCCESS
333 message = result.message
335 if 'additional_email' in form.cleaned_data:
336 additional_email = form.cleaned_data['additional_email']
337 if additional_email != user.email:
338 user.additionalmail_set.create(email=additional_email)
339 msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
340 logger._log(LOGGING_LEVEL, msg, [])
341 if user and user.is_active:
342 next = request.POST.get('next', '')
343 return prepare_response(request, user, next=next)
344 messages.add_message(request, status, message)
345 return render_response(on_success,
346 context_instance=get_context(request, extra_context))
347 except SendMailError, e:
349 messages.error(request, message)
350 except BaseException, e:
351 message = _('Something went wrong.')
352 messages.error(request, message)
354 return render_response(template_name,
357 context_instance=get_context(request, extra_context))
360 @signed_terms_required
361 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
363 Allows a user to send feedback.
365 In case of GET request renders a form for providing the feedback information.
366 In case of POST sends an email to support team.
368 If the user isn't logged in, redirects to settings.LOGIN_URL.
373 A custom template to use. This is optional; if not specified,
374 this will default to ``im/feedback.html``.
377 An dictionary of variables to add to the template context.
381 im/signup.html or ``template_name`` keyword argument.
385 * LOGIN_URL: login uri
386 * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
388 if request.method == 'GET':
389 form = FeedbackForm()
390 if request.method == 'POST':
392 return HttpResponse('Unauthorized', status=401)
394 form = FeedbackForm(request.POST)
396 msg = form.cleaned_data['feedback_msg']
397 data = form.cleaned_data['feedback_data']
399 send_feedback(msg, data, request.user, email_template_name)
400 except SendMailError, e:
401 status = messages.ERROR
402 messages.error(request, message)
404 message = _('Feedback successfully sent')
405 messages.succeess(request, message)
406 return render_response(template_name,
407 feedback_form = form,
408 context_instance = get_context(request, extra_context))
410 @signed_terms_required
411 def logout(request, template='registration/logged_out.html', extra_context={}):
413 Wraps `django.contrib.auth.logout` and delete the cookie.
415 response = HttpResponse()
416 if request.user.is_authenticated():
417 email = request.user.email
419 response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
420 msg = 'Cookie deleted for %s' % email
421 logger._log(LOGGING_LEVEL, msg, [])
422 next = request.GET.get('next')
424 response['Location'] = next
425 response.status_code = 302
428 response['Location'] = LOGOUT_NEXT
429 response.status_code = 301
431 messages.success(request, _('You have successfully logged out.'))
432 context = get_context(request, extra_context)
433 response.write(render_to_string(template, context_instance=context))
436 @transaction.commit_manually
437 def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
439 Activates the user identified by the ``auth`` request parameter, sends a welcome email
440 and renews the user token.
442 The view uses commit_manually decorator in order to ensure the user state will be updated
443 only if the email will be send successfully.
445 token = request.GET.get('auth')
446 next = request.GET.get('next')
448 user = AstakosUser.objects.get(auth_token=token)
449 except AstakosUser.DoesNotExist:
450 return HttpResponseBadRequest(_('No such user'))
453 message = _('Account already active.')
454 messages.error(request, message)
455 return index(request)
458 local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
459 except AstakosUser.DoesNotExist:
461 activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
462 response = prepare_response(request, user, next, renew=True)
465 except SendMailError, e:
467 messages.error(request, message)
468 transaction.rollback()
469 return index(request)
470 except BaseException, e:
471 message = _('Something went wrong.')
472 messages.error(request, message)
474 transaction.rollback()
475 return index(request)
478 user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
479 response = prepare_response(request, user, next, renew=True)
482 except SendMailError, e:
484 messages.error(request, message)
485 transaction.rollback()
486 return index(request)
487 except BaseException, e:
488 message = _('Something went wrong.')
489 messages.error(request, message)
491 transaction.rollback()
492 return index(request)
494 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
499 term = ApprovalTerms.objects.order_by('-id')[0]
504 term = ApprovalTerms.objects.get(id=term_id)
505 except ApprovalTermDoesNotExist, e:
509 return HttpResponseRedirect(reverse('index'))
510 f = open(term.location, 'r')
513 if request.method == 'POST':
514 next = request.POST.get('next')
516 next = reverse('index')
517 form = SignApprovalTermsForm(request.POST, instance=request.user)
518 if not form.is_valid():
519 return render_response(template_name,
521 approval_terms_form = form,
522 context_instance = get_context(request, extra_context))
524 return HttpResponseRedirect(next)
527 if request.user.is_authenticated() and not request.user.signed_terms():
528 form = SignApprovalTermsForm(instance=request.user)
529 return render_response(template_name,
531 approval_terms_form = form,
532 context_instance = get_context(request, extra_context))
534 @signed_terms_required
535 def change_password(request):
536 return password_change(request,
537 post_change_redirect=reverse('edit_profile'),
538 password_change_form=ExtendedPasswordChangeForm)
540 @signed_terms_required
542 @transaction.commit_manually
543 def change_email(request, activation_key=None,
544 email_template_name='registration/email_change_email.txt',
545 form_template_name='registration/email_change_form.html',
546 confirm_template_name='registration/email_change_done.html',
550 user = EmailChange.objects.change_email(activation_key)
551 if request.user.is_authenticated() and request.user == user:
552 msg = _('Email changed successfully.')
553 messages.success(request, msg)
555 response = prepare_response(request, user)
558 except ValueError, e:
559 messages.error(request, e)
560 return render_response(confirm_template_name,
561 modified_user = user if 'user' in locals() else None,
562 context_instance = get_context(request,
565 if not request.user.is_authenticated():
566 path = quote(request.get_full_path())
567 url = request.build_absolute_uri(reverse('index'))
568 return HttpResponseRedirect(url + '?next=' + path)
569 form = EmailChangeForm(request.POST or None)
570 if request.method == 'POST' and form.is_valid():
572 ec = form.save(email_template_name, request)
573 except SendMailError, e:
575 messages.error(request, msg)
576 transaction.rollback()
577 except IntegrityError, e:
578 msg = _('There is already a pending change email request.')
579 messages.error(request, msg)
581 msg = _('Change email request has been registered succefully.\
582 You are going to receive a verification email in the new address.')
583 messages.success(request, msg)
585 return render_response(form_template_name,
587 context_instance = get_context(request,
590 @signed_terms_required
592 def group_add(request, kind_name='default'):
594 kind = GroupKind.objects.get(name = kind_name)
596 return HttpResponseBadRequest(_('No such group kind'))
599 template_loader=loader
601 post_save_redirect='/im/group/%(id)s/'
603 context_processors=None
604 model, form_class = get_model_and_form_class(
606 form_class=AstakosGroupCreationForm
608 # TODO better approach???
609 resources = dict( (str(r.id), r) for r in Resource.objects.select_related().all() )
610 if request.method == 'POST':
611 form = form_class(request.POST, request.FILES, resources=resources)
613 new_object = form.save()
614 new_object.owners = [request.user]
615 for (rid, limit) in form.resources():
620 # Should I stay or should I go???
623 new_object.astakosgroupquota_set.create(
627 msg = _("The %(verbose_name)s was created successfully.") %\
628 {"verbose_name": model._meta.verbose_name}
629 messages.success(request, msg, fail_silently=True)
630 return redirect(post_save_redirect, new_object)
636 'expiration_date':now + timedelta(days=30)
638 form = form_class(data, resources=resources)
640 # Create the template, context, response
641 template_name = "%s/%s_form.html" % (
642 model._meta.app_label,
643 model._meta.object_name.lower()
645 t = template_loader.get_template(template_name)
646 c = RequestContext(request, {
648 }, context_processors)
649 return HttpResponse(t.render(c))
651 @signed_terms_required
653 def group_list(request):
654 list = request.user.astakos_groups.select_related().all()
655 return object_list(request, queryset=list)
657 @signed_terms_required
659 def group_detail(request, group_id):
661 group = AstakosGroup.objects.select_related().get(id=group_id)
662 except AstakosGroup.DoesNotExist:
663 return HttpResponseBadRequest(_('Invalid group.'))
664 return object_detail(request,
665 AstakosGroup.objects.all(),
667 extra_context = {'quota':group.quota}
670 @signed_terms_required
672 def group_approval_request(request, group_id):
673 return HttpResponse()
675 @signed_terms_required
677 def group_search(request, extra_context={}, **kwargs):
679 if request.method == 'GET':
680 form = AstakosGroupSearchForm()
682 form = AstakosGroupSearchForm(get_query(request))
684 q = form.cleaned_data['q'].strip()
685 q = URLField().to_python(q)
686 queryset = AstakosGroup.objects.select_related().filter(name=q)
687 f = MembershipCreationForm
689 join_forms[g.name] = f(
693 date_requested=datetime.now().strftime("%d/%m/%Y")
699 template_name='im/astakosgroup_list.html',
703 join_forms=join_forms
706 return render_response(
707 template='im/astakosgroup_list.html',
709 context_instance=get_context(request)
712 @signed_terms_required
714 def group_join(request, group_id):
715 return create_object(
718 template_name='im/astakosgroup_list.html',
719 post_save_redirect = reverse(
721 kwargs=dict(group_id=group_id)
725 @signed_terms_required
727 def group_leave(request, group_id):
729 m = Membership.objects.select_related().get(
733 except Membership.DoesNotExist:
734 return HttpResponseBadRequest(_('Invalid membership.'))
735 if request.user in m.group.owner.all():
736 return HttpResponseForbidden(_('Owner can not leave the group.'))
737 return delete_object(
741 template_name='im/astakosgroup_list.html',
742 post_delete_redirect = reverse(
744 kwargs=dict(group_id=group_id)
748 def handle_membership():
751 def wrapper(request, group_id, user_id):
753 m = Membership.objects.select_related().get(
757 except Membership.DoesNotExist:
758 return HttpResponseBadRequest(_('Invalid membership.'))
760 if request.user not in m.group.owner.all():
761 return HttpResponseForbidden(_('User is not a group owner.'))
763 return render_response(
764 template='im/astakosgroup_detail.html',
765 context_instance=get_context(request),
768 more_policies=m.group.has_undefined_policies
773 @signed_terms_required
776 def approve_member(request, membership):
779 realname = membership.person.realname
780 msg = _('%s has been successfully joined the group.' % realname)
781 messages.success(request, msg)
782 except BaseException, e:
784 msg = _('Something went wrong during %s\'s approval.' % realname)
785 messages.error(request, msg)
787 @signed_terms_required
790 def disapprove_member(request, membership):
792 membership.disapprove()
793 realname = membership.person.realname
794 msg = _('%s has been successfully removed from the group.' % realname)
795 messages.success(request, msg)
796 except BaseException, e:
798 msg = _('Something went wrong during %s\'s disapproval.' % realname)
799 messages.error(request, msg)
801 @signed_terms_required
803 def resource_list(request):
804 return render_response(
805 template='im/astakosuserquota_list.html',
806 context_instance=get_context(request),
807 quota=request.user.quota