add welcome_email template
[astakos] / astakos / im / views.py
index 368c31a..aae8927 100644 (file)
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
 
-import json
 import logging
 import socket
-import csv
-import sys
 
-from datetime import datetime
-from functools import wraps
-from math import ceil
-from random import randint
 from smtplib import SMTPException
-from hashlib import new as newhasher
 from urllib import quote
+from functools import wraps
 
-from django.conf import settings
 from django.core.mail import send_mail
-from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
+from django.http import HttpResponse
 from django.shortcuts import redirect
 from django.template.loader import render_to_string
-from django.shortcuts import render_to_response
-from django.utils.http import urlencode
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
-from django.contrib.auth.models import AnonymousUser
 from django.contrib.auth.decorators import login_required
-from django.contrib.sites.models import Site
 from django.contrib import messages
 from django.db import transaction
-from django.contrib.auth.forms import UserCreationForm
+from django.contrib.auth import logout as auth_logout
+from django.utils.http import urlencode
+from django.http import HttpResponseRedirect
 
-#from astakos.im.openid_store import PithosOpenIDStore
 from astakos.im.models import AstakosUser, Invitation
-from astakos.im.util import isoformat, get_or_create_user, get_context
 from astakos.im.backends import get_backend
-from astakos.im.forms import ProfileForm, FeedbackForm, LoginForm
+from astakos.im.util import get_context, prepare_response, set_cookie
+from astakos.im.forms import *
+from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL
+from astakos.im.functions import invite as invite_func
 
-def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
+logger = logging.getLogger(__name__)
+
+def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
     """
     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
     keyword argument and returns an ``django.http.HttpResponse`` with the
     specified ``status``.
     """
     if tab is None:
-        tab = template.partition('_')[0]
+        tab = template.partition('_')[0].partition('.html')[0]
     kwargs.setdefault('tab', tab)
     html = render_to_string(template, kwargs, context_instance=context_instance)
-    return HttpResponse(html, status=status)
+    response = HttpResponse(html, status=status)
+    if reset_cookie:
+        set_cookie(response, context_instance['request'].user)
+    return response
+
 
-def index(request, template_name='index.html', extra_context={}):
+def requires_anonymous(func):
+    """
+    Decorator checkes whether the request.user is Anonymous and in that case
+    redirects to `logout`.
     """
-    Renders the index (login) page
+    @wraps(func)
+    def wrapper(request, *args):
+        if not request.user.is_anonymous():
+            next = urlencode({'next': request.build_absolute_uri()})
+            login_uri = reverse(logout) + '?' + next
+            return HttpResponseRedirect(login_uri)
+        return func(request, *args)
+    return wrapper
+
+def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
+    """
+    If there is logged on user renders the profile page otherwise renders login page.
     
     **Arguments**
     
-    ``template_name``
-        A custom template to use. This is optional; if not specified,
-        this will default to ``index.html``.
+    ``login_template_name``
+        A custom login template to use. This is optional; if not specified,
+        this will default to ``im/login.html``.
+    
+    ``profile_template_name``
+        A custom profile template to use. This is optional; if not specified,
+        this will default to ``im/profile.html``.
     
     ``extra_context``
         An dictionary of variables to add to the template context.
     
     **Template:**
     
-    index.html or ``template_name`` keyword argument.
+    im/profile.html or im/login.html or ``template_name`` keyword argument.
     
     """
+    template_name = login_template_name
+    formclass = 'LoginForm'
+    kwargs = {}
+    if request.user.is_authenticated():
+        template_name = profile_template_name
+        formclass = 'ProfileForm'
+        kwargs.update({'instance':request.user})
     return render_response(template_name,
-                           form = LoginForm(),
+                           form = globals()[formclass](**kwargs),
                            context_instance = get_context(request, extra_context))
 
-def _generate_invitation_code():
-    while True:
-        code = randint(1, 2L**63 - 1)
-        try:
-            Invitation.objects.get(code=code)
-            # An invitation with this code already exists, try again
-        except Invitation.DoesNotExist:
-            return code
-
-def _send_invitation(request, baseurl, inv):
-    subject = _('Invitation to Astakos')
-    site = Site.objects.get_current()
-    url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(site.domain))
-    message = render_to_string('invitation.txt', {
-                'invitation': inv,
-                'url': url,
-                'baseurl': baseurl,
-                'service': site.name,
-                'support': settings.DEFAULT_CONTACT_EMAIL})
-    sender = settings.DEFAULT_FROM_EMAIL
-    send_mail(subject, message, sender, [inv.username])
-    logging.info('Sent invitation %s', inv)
-
 @login_required
 @transaction.commit_manually
-def invite(request, template_name='invitations.html', extra_context={}):
+def invite(request, template_name='im/invitations.html', extra_context={}):
     """
     Allows a user to invite somebody else.
     
@@ -143,23 +142,22 @@ def invite(request, template_name='invitations.html', extra_context={}):
     
     ``template_name``
         A custom template to use. This is optional; if not specified,
-        this will default to ``invitations.html``.
+        this will default to ``im/invitations.html``.
     
     ``extra_context``
         An dictionary of variables to add to the template context.
     
     **Template:**
     
-    invitations.html or ``template_name`` keyword argument.
+    im/invitations.html or ``template_name`` keyword argument.
     
     **Settings:**
     
     The view expectes the following settings are defined:
     
     * LOGIN_URL: login uri
-    * SIGNUP_TARGET: Where users should signup with their invitation code
-    * DEFAULT_CONTACT_EMAIL: service support email
-    * DEFAULT_FROM_EMAIL: from email
+    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
+    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
     """
     status = None
     message = None
@@ -170,18 +168,8 @@ def invite(request, template_name='invitations.html', extra_context={}):
         realname = request.POST.get('realname')
         
         if inviter.invitations > 0:
-            code = _generate_invitation_code()
-            invitation, created = Invitation.objects.get_or_create(
-                inviter=inviter,
-                username=username,
-                defaults={'code': code, 'realname': realname})
-            
             try:
-                baseurl = request.build_absolute_uri('/').rstrip('/')
-                _send_invitation(request, baseurl, invitation)
-                if created:
-                    inviter.invitations = max(0, inviter.invitations - 1)
-                    inviter.save()
+                invite_func(inviter, username, realname)
                 status = messages.SUCCESS
                 message = _('Invitation sent to %s' % username)
                 transaction.commit()
@@ -194,65 +182,72 @@ def invite(request, template_name='invitations.html', extra_context={}):
             message = _('No invitations left')
     messages.add_message(request, status, message)
     
-    if request.GET.get('format') == 'json':
-        sent = [{'email': inv.username,
-                 'realname': inv.realname,
-                 'is_accepted': inv.is_accepted}
-                    for inv in inviter.invitations_sent.all()]
-        rep = {'invitations': inviter.invitations, 'sent': sent}
-        return HttpResponse(json.dumps(rep))
-    
-    kwargs = {'user': inviter}
+    sent = [{'email': inv.username,
+             'realname': inv.realname,
+             'is_consumed': inv.is_consumed}
+             for inv in inviter.invitations_sent.all()]
+    kwargs = {'inviter': inviter,
+              'sent':sent}
     context = get_context(request, extra_context, **kwargs)
     return render_response(template_name,
                            context_instance = context)
 
 @login_required
-def edit_profile(request, template_name='profile.html', extra_context={}):
+def edit_profile(request, template_name='im/profile.html', extra_context={}):
     """
     Allows a user to edit his/her profile.
     
     In case of GET request renders a form for displaying the user information.
-    In case of POST updates the user informantion.
+    In case of POST updates the user informantion and redirects to ``next``
+    url parameter if exists.
     
-    If the user isn't logged in, redirects to settings.LOGIN_URL.  
+    If the user isn't logged in, redirects to settings.LOGIN_URL.
     
     **Arguments**
     
     ``template_name``
         A custom template to use. This is optional; if not specified,
-        this will default to ``profile.html``.
+        this will default to ``im/profile.html``.
     
     ``extra_context``
         An dictionary of variables to add to the template context.
     
     **Template:**
     
-    profile.html or ``template_name`` keyword argument.
+    im/profile.html or ``template_name`` keyword argument.
+    
+    **Settings:**
+    
+    The view expectes the following settings are defined:
+    
+    * LOGIN_URL: login uri
     """
-    try:
-        user = AstakosUser.objects.get(username=request.user)
-        form = ProfileForm(instance=user)
-    except AstakosUser.DoesNotExist:
-        token = request.GET.get('auth', None)
-        user = AstakosUser.objects.get(auth_token=token)
+    form = ProfileForm(instance=request.user)
+    extra_context['next'] = request.GET.get('next')
+    reset_cookie = False
     if request.method == 'POST':
-        form = ProfileForm(request.POST, instance=user)
+        form = ProfileForm(request.POST, instance=request.user)
         if form.is_valid():
             try:
-                form.save()
+                prev_token = request.user.auth_token
+                user = form.save()
+                reset_cookie = user.auth_token != prev_token
+                form = ProfileForm(instance=user)
+                next = request.POST.get('next')
+                if next:
+                    return redirect(next)
                 msg = _('Profile has been updated successfully')
                 messages.add_message(request, messages.SUCCESS, msg)
             except ValueError, ve:
                 messages.add_message(request, messages.ERROR, ve)
     return render_response(template_name,
+                           reset_cookie = reset_cookie,
                            form = form,
                            context_instance = get_context(request,
-                                                          extra_context,
-                                                          user=user))
+                                                          extra_context))
 
-@transaction.commit_manually
-def signup(request, template_name='signup.html', extra_context={}, backend=None):
+@requires_anonymous
+def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
     """
     Allows a user to create a local account.
     
@@ -261,7 +256,7 @@ def signup(request, template_name='signup.html', extra_context={}, backend=None)
     
     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
     if present, otherwise to the ``astakos.im.backends.InvitationBackend``
-    if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
+    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
     (see backends);
     
     Upon successful user creation if ``next`` url parameter is present the user is redirected there
@@ -269,70 +264,84 @@ def signup(request, template_name='signup.html', extra_context={}, backend=None)
     
     On unsuccessful creation, renders the same page with an error message.
     
-    The view uses commit_manually decorator in order to ensure the user will be created
-    only if the procedure has been completed successfully.
-    
     **Arguments**
     
-    ``template_name``
-        A custom template to use. This is optional; if not specified,
-        this will default to ``signup.html``.
+    ``on_failure``
+        A custom template to render in case of failure. This is optional;
+        if not specified, this will default to ``im/signup.html``.
+    
+    
+    ``on_success``
+        A custom template to render in case of success. This is optional;
+        if not specified, this will default to ``im/signup_complete.html``.
     
     ``extra_context``
         An dictionary of variables to add to the template context.
     
     **Template:**
     
-    signup.html or ``template_name`` keyword argument.
+    im/signup.html or ``on_failure`` keyword argument.
+    im/signup_complete.html or ``on_success`` keyword argument. 
     """
-    if not backend:
-        backend = get_backend(request)
     try:
-        form = backend.get_signup_form()
+        if not backend:
+            backend = get_backend(request)
+        for provider in IM_MODULES:
+            extra_context['%s_form' % provider] = backend.get_signup_form(provider)
         if request.method == 'POST':
+            provider = request.POST.get('provider')
+            next = request.POST.get('next', '')
+            form = extra_context['%s_form' % provider]
             if form.is_valid():
-                status, message = backend.signup(form)
-                # rollback in case of error
-                if status == messages.ERROR:
-                    transaction.rollback()
+                if provider != 'local':
+                    url = reverse('astakos.im.target.%s.login' % provider)
+                    url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
+                    if backend.invitation:
+                        url = '%s&code=%s' % (url, backend.invitation.code)
+                    return redirect(url)
                 else:
-                    transaction.commit()
-                    next = request.POST.get('next')
-                    if next:
-                        return redirect(next)
-                messages.add_message(request, status, message)
-    except Invitation.DoesNotExist, e:
+                    status, message, user = backend.signup(form)
+                    if user and user.is_active:
+                        return prepare_response(request, user, next=next)
+                    messages.add_message(request, status, message)
+                    return render_response(on_success,
+                           context_instance=get_context(request, extra_context))
+    except (Invitation.DoesNotExist, ValueError), e:
         messages.add_message(request, messages.ERROR, e)
-    return render_response(template_name,
-                           form = form if 'form' in locals() else UserCreationForm(),
+        for provider in IM_MODULES:
+            main = provider.capitalize() if provider == 'local' else 'ThirdParty'
+            formclass = '%sUserCreationForm' % main
+            extra_context['%s_form' % provider] = globals()[formclass]()
+    return render_response(on_failure,
                            context_instance=get_context(request, extra_context))
 
 @login_required
-def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
+def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
     """
     Allows a user to send feedback.
     
     In case of GET request renders a form for providing the feedback information.
     In case of POST sends an email to support team.
     
-    If the user isn't logged in, redirects to settings.LOGIN_URL.  
+    If the user isn't logged in, redirects to settings.LOGIN_URL.
     
     **Arguments**
     
     ``template_name``
         A custom template to use. This is optional; if not specified,
-        this will default to ``feedback.html``.
+        this will default to ``im/feedback.html``.
     
     ``extra_context``
         An dictionary of variables to add to the template context.
     
     **Template:**
     
-    signup.html or ``template_name`` keyword argument.
+    im/signup.html or ``template_name`` keyword argument.
     
     **Settings:**
     
-    * FEEDBACK_CONTACT_EMAIL: List of feedback recipients
+    * LOGIN_URL: login uri
+    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
     """
     if request.method == 'GET':
         form = FeedbackForm()
@@ -342,18 +351,53 @@ def send_feedback(request, template_name='feedback.html', email_template_name='f
         
         form = FeedbackForm(request.POST)
         if form.is_valid():
-            subject = _("Feedback from Okeanos")
+            subject = _("Feedback from %s" % SITENAME)
             from_email = request.user.email
-            recipient_list = [settings.FEEDBACK_CONTACT_EMAIL]
+            recipient_list = [DEFAULT_CONTACT_EMAIL]
             content = render_to_string(email_template_name, {
-                        'message': form.cleaned_data('feedback_msg'),
-                        'data': form.cleaned_data('feedback_data'),
+                        'message': form.cleaned_data['feedback_msg'],
+                        'data': form.cleaned_data['feedback_data'],
                         'request': request})
             
-            send_mail(subject, content, from_email, recipient_list)
-            
-            resp = json.dumps({'status': 'send'})
-            return HttpResponse(resp)
+            try:
+                send_mail(subject, content, from_email, recipient_list)
+                message = _('Feedback successfully sent')
+                status = messages.SUCCESS
+            except (SMTPException, socket.error) as e:
+                status = messages.ERROR
+                message = getattr(e, 'strerror', '')
+            messages.add_message(request, status, message)
     return render_response(template_name,
                            form = form,
                            context_instance = get_context(request, extra_context))
+
+def logout(request, template='registration/logged_out.html', extra_context={}):
+    """
+    Wraps `django.contrib.auth.logout` and delete the cookie.
+    """
+    auth_logout(request)
+    response = HttpResponse()
+    response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
+    next = request.GET.get('next')
+    if next:
+        response['Location'] = next
+        response.status_code = 302
+        return response
+    context = get_context(request, extra_context)
+    response.write(render_to_string(template, context_instance=context))
+    return response
+
+def activate(request):
+    """
+    Activates the user identified by the ``auth`` request parameter
+    """
+    token = request.GET.get('auth')
+    next = request.GET.get('next')
+    try:
+        user = AstakosUser.objects.get(auth_token=token)
+    except AstakosUser.DoesNotExist:
+        return HttpResponseBadRequest('No such user')
+    
+    user.is_active = True
+    user.save()
+    return prepare_response(request, user, next, renew=True)