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)),