From 05929b22721ee25fcc53b82222608f82d33cf983 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emilio=20A=2E=20S=C3=A1nchez=20L=C3=B3pez?=
 <esanchez@yaco.es>
Date: Wed, 13 Jul 2011 10:19:26 +0000
Subject: [PATCH] User registration system. See #688  - Legacy-Id: 3195

---
 ietf/registration/__init__.py                 |   0
 ietf/registration/forms.py                    | 114 ++++++++++++++++++
 ietf/registration/urls.py                     |  10 ++
 ietf/registration/views.py                    |  93 ++++++++++++++
 ietf/settings.py                              |   4 +
 ietf/templates/registration/base.html         |   6 +
 .../registration/change_password.html         |  23 ++++
 .../registration/confirm_register.html        |  23 ++++
 .../registration/password_recovery.html       |  22 ++++
 .../registration/password_recovery_email.txt  |  13 ++
 ietf/templates/registration/register.html     |  58 +++++++++
 .../templates/registration/register_email.txt |  11 ++
 ietf/urls.py                                  |   1 +
 13 files changed, 378 insertions(+)
 create mode 100644 ietf/registration/__init__.py
 create mode 100644 ietf/registration/forms.py
 create mode 100644 ietf/registration/urls.py
 create mode 100644 ietf/registration/views.py
 create mode 100644 ietf/templates/registration/base.html
 create mode 100644 ietf/templates/registration/change_password.html
 create mode 100644 ietf/templates/registration/confirm_register.html
 create mode 100644 ietf/templates/registration/password_recovery.html
 create mode 100644 ietf/templates/registration/password_recovery_email.txt
 create mode 100644 ietf/templates/registration/register.html
 create mode 100644 ietf/templates/registration/register_email.txt

diff --git a/ietf/registration/__init__.py b/ietf/registration/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/ietf/registration/forms.py b/ietf/registration/forms.py
new file mode 100644
index 000000000..d5c98ecd6
--- /dev/null
+++ b/ietf/registration/forms.py
@@ -0,0 +1,114 @@
+import datetime
+import hashlib
+
+from django import forms
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.auth.forms import PasswordResetForm
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+from ietf.utils.mail import send_mail
+from redesign.person.models import Person, Email
+
+
+class RegistrationForm(forms.Form):
+
+    email = forms.EmailField(label="Your email")
+    realm = 'IETF'
+    expire = 3
+
+    def save(self, *args, **kwargs):
+        self.send_email()
+        return True
+
+    def send_email(self):
+        domain = Site.objects.get_current().domain
+        subject = ugettext(u'Confirm registration at %s') % domain
+        from_email = settings.DEFAULT_FROM_EMAIL
+        to_email = self.cleaned_data['email']
+        today = datetime.date.today().strftime('%Y%m%d')
+        auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, self.realm)).hexdigest()
+        context = {
+            'domain': domain,
+            'today': today,
+            'realm': self.realm,
+            'auth': auth,
+            'to_email': to_email,
+            'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
+        }
+        send_mail(None, to_email, from_email, subject, 'registration/register_email.txt', context)
+
+    def clean_email(self):
+        email = self.cleaned_data.get('email', '')
+        if not email:
+            return email
+        if User.objects.filter(username=email).count():
+            raise forms.ValidationError(_('Email already in use'))
+        return email
+
+
+class RecoverPasswordForm(PasswordResetForm):
+
+    realm = 'IETF'
+
+    def save(self):
+        domain = Site.objects.get_current().domain
+        subject = 'Password recovery at %s' % domain
+        from_email = settings.DEFAULT_FROM_EMAIL
+        today = datetime.date.today().strftime('%Y%m%d')
+        for user in self.users_cache:
+            to_email = self.cleaned_data["email"]
+            recovery_hash = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, today, user.username, user.password, self.realm)).hexdigest()
+            context = {'domain': domain,
+                       'username': user.username,
+                       'recovery_hash': recovery_hash,
+                       'today': today,
+                       'realm': self.realm,
+                       'expire': settings.DAYS_TO_EXPIRE_RECOVER_LINK,
+                      }
+            send_mail(None, to_email, from_email, subject, 'registration/password_recovery_email.txt', context)
+
+
+class PasswordForm(forms.Form):
+
+    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
+    password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput,
+        help_text=_("Enter the same password as above, for verification."))
+
+    def __init__(self, *args, **kwargs):
+        self.username = kwargs.pop('username')
+        self.update_user = kwargs.pop('update_user', False)
+        super(PasswordForm, self).__init__(*args, **kwargs)
+
+    def clean_password2(self):
+        password1 = self.cleaned_data.get("password1", "")
+        password2 = self.cleaned_data["password2"]
+        if password1 != password2:
+            raise forms.ValidationError(_("The two password fields didn't match."))
+        return password2
+
+    def get_password(self):
+        return self.cleaned_data.get('password1')
+
+    def create_user(self):
+        user = User.objects.create(username=self.username,
+                                   email=self.username)
+        person = Person.objects.create(user=user,
+                                       name=self.username,
+                                       ascii=self.username)
+        Email.objects.create(person=person,
+                             address=self.username)
+        return user
+
+    def get_user(self):
+        return User.objects.get(username=self.username)
+
+    def save(self):
+        if self.update_user:
+            user = self.get_user()
+        else:
+            user = self.create_user()
+        user.set_password(self.get_password())
+        user.save()
+        return user
diff --git a/ietf/registration/urls.py b/ietf/registration/urls.py
new file mode 100644
index 000000000..7eba5e2d5
--- /dev/null
+++ b/ietf/registration/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('ietf.registration.views',
+    url(r'^$', 'register_view', name='register_view'),
+    url(r'^confirm/(?P<username>[\w.@+-]+)/(?P<date>[\d]+)/(?P<realm>[\w]+)/(?P<registration_hash>[a-f0-9]+)/$', 'confirm_register_view', name='confirm_register_view'),
+    url(r'^password_recovery/$', 'password_recovery_view', name='password_recovery_view'),
+    url(r'^password_recovery/confirm/(?P<username>[\w.@+-]+)/(?P<date>[\d]+)/(?P<realm>[\w]+)/(?P<recovery_hash>[a-f0-9]+)/$', 'confirm_password_recovery', name='confirm_password_recovery'),
+    url(r'^ajax/check_username/$', 'ajax_check_username', name='ajax_check_username'),
+)
diff --git a/ietf/registration/views.py b/ietf/registration/views.py
new file mode 100644
index 000000000..981875caf
--- /dev/null
+++ b/ietf/registration/views.py
@@ -0,0 +1,93 @@
+import datetime
+import hashlib
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.http import HttpResponse, Http404
+from django.shortcuts import get_object_or_404, render_to_response
+from django.template import RequestContext
+from django.utils import simplejson
+from django.utils.translation import ugettext as _
+
+from ietf.registration.forms import (RegistrationForm, PasswordForm,
+                                     RecoverPasswordForm)
+
+
+def register_view(request):
+    success = False
+    if request.method == 'POST':
+        form = RegistrationForm(request.POST)
+        if form.is_valid():
+            form.save()
+            success = True
+    else:
+        form = RegistrationForm()
+    return render_to_response('registration/register.html',
+                              {'form': form,
+                               'success': success},
+                              context_instance=RequestContext(request))
+
+
+def confirm_register_view(request, username, date, realm, registration_hash):
+    valid = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, date, username, realm)).hexdigest() == registration_hash
+    if not valid:
+        raise Http404
+    request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:]))
+    if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK)):
+        raise Http404
+    success = False
+    if request.method == 'POST':
+        form = PasswordForm(request.POST, username=username)
+        if form.is_valid():
+            form.save()
+            # TODO: Add the user in the htdigest file
+            success = True
+    else:
+        form = PasswordForm(username=username)
+    return render_to_response('registration/confirm_register.html',
+                              {'form': form, 'email': username, 'success': success},
+                              context_instance=RequestContext(request))
+
+
+def password_recovery_view(request):
+    success = False
+    if request.method == 'POST':
+        form = RecoverPasswordForm(request.POST)
+        if form.is_valid():
+            form.save()
+            success = True
+    else:
+        form = RecoverPasswordForm()
+    return render_to_response('registration/password_recovery.html',
+                              {'form': form,
+                               'success': success},
+                              context_instance=RequestContext(request))
+
+
+def confirm_password_recovery(request, username, date, realm, recovery_hash):
+    user = get_object_or_404(User, username=username)
+    valid = hashlib.md5('%s%s%s%s%s' % (settings.SECRET_KEY, date, user.username, user.password, realm)).hexdigest() == recovery_hash
+    if not valid:
+        raise Http404
+    success = False
+    if request.method == 'POST':
+        form = PasswordForm(request.POST, update_user=True, username=user.username)
+        if form.is_valid():
+            user = form.save()
+            # TODO: Update the user in the htdigest file
+            success = True
+    else:
+        form = PasswordForm(username=user.username)
+    return render_to_response('registration/change_password.html',
+                              {'form': form,
+                               'success': success,
+                               'username': user.username},
+                              context_instance=RequestContext(request))
+
+
+def ajax_check_username(request):
+    username = request.GET.get('username', '')
+    error = False
+    if User.objects.filter(username=username).count():
+        error = _('This email is already in use')
+    return HttpResponse(simplejson.dumps({'error': error}), mimetype='text/plain')
diff --git a/ietf/settings.py b/ietf/settings.py
index bd3f06720..0a0545770 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -195,6 +195,10 @@ LIAISON_UNIVERSAL_FROM = 'Liaison Statement Management Tool <lsmt@' + IETF_DOMAI
 LIAISON_ATTACH_PATH = '/a/www/ietf-datatracker/documents/LIAISON/'
 LIAISON_ATTACH_URL = '/documents/LIAISON/'
 
+# Registration configuration
+DAYS_TO_EXPIRE_REGISTRATION_LINK = 3
+DAYS_TO_EXPIRE_RECOVER_LINK = 3
+
 # DB redesign
 USE_DB_REDESIGN_PROXY_CLASSES = True
 
diff --git a/ietf/templates/registration/base.html b/ietf/templates/registration/base.html
new file mode 100644
index 000000000..8c06ed64e
--- /dev/null
+++ b/ietf/templates/registration/base.html
@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+
+{% block morecss %}
+table.register-form ul.errorlist{ list-style-type: none; color: red; padding: 0px; margin: 0px; }
+table.register-form p { margin-top: 0px; }
+{% endblock %}
diff --git a/ietf/templates/registration/change_password.html b/ietf/templates/registration/change_password.html
new file mode 100644
index 000000000..ac792645a
--- /dev/null
+++ b/ietf/templates/registration/change_password.html
@@ -0,0 +1,23 @@
+{% extends "registration/base.html" %}
+
+{% block title %}Change password{% endblock %}
+
+{% block content %}
+<div id="change_password_page">
+<h1>Change password</h1>
+{% if success %}
+<p>Your password has been updated.</p>
+<p>Now you can <a href="{% url ietfauth.views.ietf_login %}">sign in</a></p>
+{% else %}
+<p>Hello, you can select a new password below for your user {{ username }}.</p>
+<form action="" method="POST">
+  <table class="register-form">
+  {{ form }}
+  </table>
+  <div class="submit_row">
+    <input type="submit" value="Change password" />
+  </div>
+</form>
+{% endif %}
+</div>
+{% endblock %}
diff --git a/ietf/templates/registration/confirm_register.html b/ietf/templates/registration/confirm_register.html
new file mode 100644
index 000000000..e78f4ccfe
--- /dev/null
+++ b/ietf/templates/registration/confirm_register.html
@@ -0,0 +1,23 @@
+{% extends "registration/base.html" %}
+
+{% block title %}Confirm registration{% endblock %}
+
+{% block content %}
+<div id="confirm_register_page">
+<h1>Confirm registration</h1>
+{% if success %}
+<p>Your email {{ email }} has been registered using the password you have select.</p>
+<p>Now you can <a href="{% url ietfauth.views.ietf_login %}">sign in</a></p>
+{% else %}
+<p>Hello, the registration for {{ email }} is almost complete. Please, select a password.</p>
+<form action="" method="POST">
+  <table class="register-form">
+  {{ form }}
+  </table>
+  <div class="submit_row">
+    <input type="submit" value="Register" />
+  </div>
+</form>
+{% endif %}
+</div>
+{% endblock %}
diff --git a/ietf/templates/registration/password_recovery.html b/ietf/templates/registration/password_recovery.html
new file mode 100644
index 000000000..c327cad62
--- /dev/null
+++ b/ietf/templates/registration/password_recovery.html
@@ -0,0 +1,22 @@
+{% extends "registration/base.html" %}
+
+{% block title %}Password recovery{% endblock %}
+
+{% block content %}
+<div id="register_page">
+<h1>Password recovery</h1>
+{% if success %}
+<p>Your password recovery request has been received successfully. We have sent you an email with instructions on how to change your password.</p>
+<p>Thank you.</p>
+{% else %}
+<form action="" method="POST">
+  <table class="register-form">
+  {{ form }}
+  </table>
+  <div class="submit_row">
+    <input type="submit" value="Recover my password" />
+  </div>
+</form>
+{% endif %}
+</div>
+{% endblock %}
diff --git a/ietf/templates/registration/password_recovery_email.txt b/ietf/templates/registration/password_recovery_email.txt
new file mode 100644
index 000000000..471fbacb9
--- /dev/null
+++ b/ietf/templates/registration/password_recovery_email.txt
@@ -0,0 +1,13 @@
+Hello,
+
+We have received a password recovery request at {{ domain }}. In order to change the password for the user with username {{ username }} please follow or paste and copy in your browser the follwoing link:
+
+http://{{ domain }}{% url confirm_password_recovery username today realm recovery_hash %}
+
+This link will expire in {{ expire }} days.
+
+If you didn't request a password recovery you can ignore this email, your credentials have been left untouched.
+
+Best,
+
+Your {{ domain }} team.
diff --git a/ietf/templates/registration/register.html b/ietf/templates/registration/register.html
new file mode 100644
index 000000000..c37a3746f
--- /dev/null
+++ b/ietf/templates/registration/register.html
@@ -0,0 +1,58 @@
+{% extends "registration/base.html" %}
+
+{% block title %}Register{% endblock %}
+
+{% block scripts %}
+  {{ block.super }}
+
+(function($) {
+    var checkUsername = function() {
+        var field = $(this);
+        var url = $("#check_user_name_url").val();
+        var error = field.next('.username-error');
+
+        $.ajax({
+            url: url,
+            data: {username: field.val()},
+            dataType: 'json',
+            success: function(response) {
+                if (response.error) {
+                   error.text(response.error);
+                   error.show();
+                } else {
+                   error.hide();
+                }
+            }
+        });
+    }
+
+    $(document).ready(function(){
+        $('#id_email').after(' <span class="username-error" style="display: none;"></span>');
+        $('#id_email').keyup(checkUsername).blur(checkUsername);
+    });
+})(jQuery);
+{% endblock %}
+
+{% block content %}
+<div id="register_page">
+<h1>Register</h1>
+{% if success %}
+<p>Your registration request has been received successfully. We have sent you an email with instructions on how to finish the registration process.</p>
+<p>Thank you.</p>
+{% else %}
+<form action="" method="POST">
+  <p>Please enter your email addres in order to register a new account.</p>
+  <table class="register-form">
+  {{ form }}
+  </table>
+  <div class="submit_row">
+    <input type="hidden" id="check_user_name_url" value="{% url ajax_check_username %}" />
+    <input type="submit" value="Register" />
+  </div>
+</form>
+<p class="recover_password_description">
+I'm already registered but I forgot my password. <a href="{% url password_recovery_view %}">Please, help me recover my password.</a>
+</p>
+{% endif %}
+</div>
+{% endblock %}
diff --git a/ietf/templates/registration/register_email.txt b/ietf/templates/registration/register_email.txt
new file mode 100644
index 000000000..9b4663a30
--- /dev/null
+++ b/ietf/templates/registration/register_email.txt
@@ -0,0 +1,11 @@
+Hello,
+
+In order to complete your registration on {{ domain }}, please follow this link or copy it and paste it in your web browser:
+
+http://{{ domain }}{% url confirm_register_view to_email today realm auth %}
+
+This link will expire in {{ expire }} days.
+
+Best,
+
+Your {{ domain }} team.
diff --git a/ietf/urls.py b/ietf/urls.py
index 215b981bd..f81fbbcb2 100644
--- a/ietf/urls.py
+++ b/ietf/urls.py
@@ -58,6 +58,7 @@ urlpatterns = patterns('',
     (r'^accounts/', include('ietf.ietfauth.urls')),
     (r'^doc/', include('ietf.idrfc.urls')),
     (r'^wg/', include('ietf.wginfo.urls')),
+    (r'^registration/', include('ietf.registration.urls')),
 
     (r'^$', 'ietf.idrfc.views.main'),
     ('^admin/', include(admin.site.urls)),