Fixed #7723 - implemented a secure password reset form that uses a token and prompts user for new password.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8162 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Luke Plant 2008-07-31 20:47:53 +00:00
parent 9a56fe765e
commit fcd837cd0f
16 changed files with 401 additions and 70 deletions

View File

@ -366,6 +366,9 @@ LOGOUT_URL = '/accounts/logout/'
LOGIN_REDIRECT_URL = '/accounts/profile/' LOGIN_REDIRECT_URL = '/accounts/profile/'
# The number of days a password reset link is valid for
PASSWORD_RESET_TIMEOUT_DAYS = 3
########### ###########
# TESTING # # TESTING #
########### ###########

View File

@ -0,0 +1,14 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {% trans 'Password reset' %}</div>{% endblock %}
{% block title %}{% trans 'Password reset complete' %}{% endblock %}
{% block content %}
<h1>{% trans 'Password reset complete' %}</h1>
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> &rsaquo; {% trans 'Password reset confirmation' %}</div>{% endblock %}
{% block title %}{% trans 'Password reset' %}{% endblock %}
{% block content %}
{% if validlink %}
<h1>{% trans 'Enter new password' %}</h1>
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form action="" method="post">
{% if form.new_password1.errors %}{{ form.new_password1.errors }}{% endif %}
<p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p>
{% if form.new_password2.errors %}{{ form.new_password2.errors }}{% endif %}
<p class="aligned wide"><label for="id_new_password2">{% trans 'Confirm password:' %}</label>{{ form.new_password2 }}</p>
<p><input type="submit" value="{% trans 'Change my password' %}" /></p>
</form>
{% else %}
<h1>{% trans 'Password reset unsuccessful' %}</h1>
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}
{% endif %}
{% endblock %}

View File

@ -9,6 +9,6 @@
<h1>{% trans 'Password reset successful' %}</h1> <h1>{% trans 'Password reset successful' %}</h1>
<p>{% trans "We've e-mailed a new password to the e-mail address you submitted. You should be receiving it shortly." %}</p> <p>{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}</p>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,15 @@
{% load i18n %} {% load i18n %}{% autoescape off %}
{% trans "You're receiving this e-mail because you requested a password reset" %} {% trans "You're receiving this e-mail because you requested a password reset" %}
{% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}. {% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}.
{% blocktrans %}Your new password is: {{ new_password }}{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{% trans "Feel free to change this password by going to this page:" %} {{ protocol }}://{{ domain }}/reset/{{ uid }}-{{ token }}/
{% endblock %}
http://{{ domain }}/password_change/
{% trans "Your username, in case you've forgotten:" %} {{ user.username }} {% trans "Your username, in case you've forgotten:" %} {{ user.username }}
{% trans "Thanks for using our site!" %} {% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

View File

@ -9,7 +9,7 @@
<h1>{% trans "Password reset" %}</h1> <h1>{% trans "Password reset" %}</h1>
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}</p> <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p>
<form action="" method="post"> <form action="" method="post">
{% if form.email.errors %}{{ form.email.errors }}{% endif %} {% if form.email.errors %}{{ form.email.errors }}{% endif %}

View File

@ -1,9 +1,11 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.template import Context, loader from django.template import Context, loader
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.http import int_to_base36
class UserCreationForm(forms.ModelForm): class UserCreationForm(forms.ModelForm):
""" """
@ -98,15 +100,13 @@ class PasswordResetForm(forms.Form):
if len(self.users_cache) == 0: if len(self.users_cache) == 0:
raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?")) raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?"))
def save(self, domain_override=None, email_template_name='registration/password_reset_email.html'): def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator):
""" """
Calculates a new password randomly and sends it to the user. Generates a one-use only link for restting password and sends to the user
""" """
from django.core.mail import send_mail from django.core.mail import send_mail
for user in self.users_cache: for user in self.users_cache:
new_pass = User.objects.make_random_password()
user.set_password(new_pass)
user.save()
if not domain_override: if not domain_override:
current_site = Site.objects.get_current() current_site = Site.objects.get_current()
site_name = current_site.name site_name = current_site.name
@ -115,35 +115,28 @@ class PasswordResetForm(forms.Form):
site_name = domain = domain_override site_name = domain = domain_override
t = loader.get_template(email_template_name) t = loader.get_template(email_template_name)
c = { c = {
'new_password': new_pass,
'email': user.email, 'email': user.email,
'domain': domain, 'domain': domain,
'site_name': site_name, 'site_name': site_name,
'uid': int_to_base36(user.id),
'user': user, 'user': user,
'token': token_generator.make_token(user),
'protocol': use_https and 'https' or 'http',
} }
send_mail(_("Password reset on %s") % site_name, send_mail(_("Password reset on %s") % site_name,
t.render(Context(c)), None, [user.email]) t.render(Context(c)), None, [user.email])
class PasswordChangeForm(forms.Form): class SetPasswordForm(forms.Form):
""" """
A form that lets a user change his/her password. A form that lets a user change set his/her password without
entering the old password
""" """
old_password = forms.CharField(label=_("Old password"), max_length=30, widget=forms.PasswordInput) new_password1 = forms.CharField(label=_("New password"), max_length=60, widget=forms.PasswordInput)
new_password1 = forms.CharField(label=_("New password"), max_length=30, widget=forms.PasswordInput) new_password2 = forms.CharField(label=_("New password confirmation"), max_length=60, widget=forms.PasswordInput)
new_password2 = forms.CharField(label=_("New password confirmation"), max_length=30, widget=forms.PasswordInput)
def __init__(self, user, *args, **kwargs): def __init__(self, user, *args, **kwargs):
self.user = user self.user = user
super(PasswordChangeForm, self).__init__(*args, **kwargs) super(SetPasswordForm, self).__init__(*args, **kwargs)
def clean_old_password(self):
"""
Validates that the old_password field is correct.
"""
old_password = self.cleaned_data["old_password"]
if not self.user.check_password(old_password):
raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again."))
return old_password
def clean_new_password2(self): def clean_new_password2(self):
password1 = self.cleaned_data.get('new_password1') password1 = self.cleaned_data.get('new_password1')
@ -159,6 +152,23 @@ class PasswordChangeForm(forms.Form):
self.user.save() self.user.save()
return self.user return self.user
class PasswordChangeForm(SetPasswordForm):
"""
A form that lets a user change his/her password by entering
their old password.
"""
old_password = forms.CharField(label=_("Old password"), max_length=60, widget=forms.PasswordInput)
def clean_old_password(self):
"""
Validates that the old_password field is correct.
"""
old_password = self.cleaned_data["old_password"]
if not self.user.check_password(old_password):
raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again."))
return old_password
PasswordChangeForm.base_fields.keyOrder = ['old_password', 'new_password1', 'new_password2']
class AdminPasswordChangeForm(forms.Form): class AdminPasswordChangeForm(forms.Form):
""" """
A form used to change the password of a user in the admin interface. A form used to change the password of a user in the admin interface.

View File

@ -1,8 +1,11 @@
from django.contrib.auth.tests.basic import BASIC_TESTS, PasswordResetTest from django.contrib.auth.tests.basic import BASIC_TESTS
from django.contrib.auth.tests.views import PasswordResetTest
from django.contrib.auth.tests.forms import FORM_TESTS from django.contrib.auth.tests.forms import FORM_TESTS
from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
__test__ = { __test__ = {
'BASIC_TESTS': BASIC_TESTS, 'BASIC_TESTS': BASIC_TESTS,
'PASSWORDRESET_TESTS': PasswordResetTest, 'PASSWORDRESET_TESTS': PasswordResetTest,
'FORM_TESTS': FORM_TESTS, 'FORM_TESTS': FORM_TESTS,
'TOKEN_GENERATOR_TESTS': TOKEN_GENERATOR_TESTS
} }

View File

@ -54,24 +54,3 @@ u'joe@somewhere.org'
>>> u.password >>> u.password
u'!' u'!'
""" """
from django.test import TestCase
from django.core import mail
class PasswordResetTest(TestCase):
fixtures = ['authtestdata.json']
urls = 'django.contrib.auth.urls'
def test_email_not_found(self):
"Error is raised if the provided email address isn't currently registered"
response = self.client.get('/password_reset/')
self.assertEquals(response.status_code, 200)
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
self.assertContains(response, "That e-mail address doesn't have an associated user account")
self.assertEquals(len(mail.outbox), 0)
def test_email_found(self):
"Email is sent if a valid email address is provided for password reset"
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEquals(response.status_code, 302)
self.assertEquals(len(mail.outbox), 1)

View File

@ -2,7 +2,7 @@
FORM_TESTS = """ FORM_TESTS = """
>>> from django.contrib.auth.models import User >>> from django.contrib.auth.models import User
>>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm >>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
>>> from django.contrib.auth.forms import PasswordChangeForm >>> from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
The user already exists. The user already exists.
@ -95,6 +95,32 @@ True
>>> form.non_field_errors() >>> form.non_field_errors()
[] []
SetPasswordForm:
The two new passwords do not match.
>>> data = {
... 'new_password1': 'abc123',
... 'new_password2': 'abc',
... }
>>> form = SetPasswordForm(user, data)
>>> form.is_valid()
False
>>> form["new_password2"].errors
[u"The two password fields didn't match."]
The success case.
>>> data = {
... 'new_password1': 'abc123',
... 'new_password2': 'abc123',
... }
>>> form = SetPasswordForm(user, data)
>>> form.is_valid()
True
PasswordChangeForm:
The old password is incorrect. The old password is incorrect.
>>> data = { >>> data = {
@ -132,4 +158,9 @@ The success case.
>>> form.is_valid() >>> form.is_valid()
True True
Regression test - check the order of fields:
>>> PasswordChangeForm(user, {}).fields.keys()
['old_password', 'new_password1', 'new_password2']
""" """

View File

@ -0,0 +1,29 @@
TOKEN_GENERATOR_TESTS = """
>>> from django.contrib.auth.models import User, AnonymousUser
>>> from django.contrib.auth.tokens import PasswordResetTokenGenerator
>>> from django.conf import settings
>>> u = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw')
>>> p0 = PasswordResetTokenGenerator()
>>> tk1 = p0.make_token(u)
>>> p0.check_token(u, tk1)
True
Tests to ensure we can use the token after n days, but no greater.
Use a mocked version of PasswordResetTokenGenerator so we can change
the value of 'today'
>>> class Mocked(PasswordResetTokenGenerator):
... def __init__(self, today):
... self._today_val = today
... def _today(self):
... return self._today_val
>>> from datetime import date, timedelta
>>> p1 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS))
>>> p1.check_token(u, tk1)
True
>>> p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1))
>>> p2.check_token(u, tk1)
False
"""

View File

@ -0,0 +1,88 @@
import re
from django.contrib.auth.models import User
from django.test import TestCase
from django.core import mail
class PasswordResetTest(TestCase):
fixtures = ['authtestdata.json']
urls = 'django.contrib.auth.urls'
def test_email_not_found(self):
"Error is raised if the provided email address isn't currently registered"
response = self.client.get('/password_reset/')
self.assertEquals(response.status_code, 200)
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
self.assertContains(response, "That e-mail address doesn't have an associated user account")
self.assertEquals(len(mail.outbox), 0)
def test_email_found(self):
"Email is sent if a valid email address is provided for password reset"
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEquals(response.status_code, 302)
self.assertEquals(len(mail.outbox), 1)
self.assert_("http://" in mail.outbox[0].body)
def _test_confirm_start(self):
# Start by creating the email
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEquals(response.status_code, 302)
self.assertEquals(len(mail.outbox), 1)
return self._read_signup_email(mail.outbox[0])
def _read_signup_email(self, email):
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
self.assert_(urlmatch is not None, "No URL found in sent email")
return urlmatch.group(), urlmatch.groups()[0]
def test_confirm_valid(self):
url, path = self._test_confirm_start()
response = self.client.get(path)
# redirect to a 'complete' page:
self.assertEquals(response.status_code, 200)
self.assert_("Please enter your new password" in response.content)
def test_confirm_invalid(self):
url, path = self._test_confirm_start()
# Lets munge the token in the path, but keep the same length,
# in case the URL conf will reject a different length
path = path[:-5] + ("0"*4) + path[-1]
response = self.client.get(path)
self.assertEquals(response.status_code, 200)
self.assert_("The password reset link was invalid" in response.content)
def test_confirm_invalid_post(self):
# Same as test_confirm_invalid, but trying
# to do a POST instead.
url, path = self._test_confirm_start()
path = path[:-5] + ("0"*4) + path[-1]
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2':' anewpassword'})
# Check the password has not been changed
u = User.objects.get(email='staffmember@example.com')
self.assert_(not u.check_password("anewpassword"))
def test_confirm_complete(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2': 'anewpassword'})
# It redirects us to a 'complete' page:
self.assertEquals(response.status_code, 302)
# Check the password has been changed
u = User.objects.get(email='staffmember@example.com')
self.assert_(u.check_password("anewpassword"))
# Check we can't use the link again
response = self.client.get(path)
self.assertEquals(response.status_code, 200)
self.assert_("The password reset link was invalid" in response.content)
def test_confirm_different_passwords(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2':' x'})
self.assertEquals(response.status_code, 200)
self.assert_("The two password fields didn't match" in response.content)

View File

@ -0,0 +1,66 @@
from datetime import date
from django.conf import settings
from django.utils.http import int_to_base36, base36_to_int
class PasswordResetTokenGenerator(object):
"""
Stratgy object used to generate and check tokens for the password
reset mechanism.
"""
def make_token(self, user):
"""
Returns a token that can be used once to do a password reset
for the given user.
"""
return self._make_token_with_timestamp(user, self._num_days(self._today()))
def check_token(self, user, token):
"""
Check that a password reset token is correct for a given user.
"""
# Parse the tokem
try:
ts_b36, hash = token.split("-")
except ValueError:
return False
try:
ts = base36_to_int(ts_b36)
except ValueError:
return False
# Check that the timestamp/uid has not been tampered with
if self._make_token_with_timestamp(user, ts) != token:
return False
# Check the timestamp is within limit
if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
return False
return True
def _make_token_with_timestamp(self, user, timestamp):
# timestamp is number of days since 2001-1-1. Converted to
# base 36, this gives us a 3 digit string until about 2121
ts_b36 = int_to_base36(timestamp)
# By hashing on the internal state of the user and using state
# that is sure to change (the password salt will change as soon as
# the password is set, at least for current Django auth, and
# last_login will also change), we produce a hash that will be
# invalid as soon as it is used.
# We limit the hash to 20 chars to keep URL short
import sha
hash = sha.new(settings.SECRET_KEY + unicode(user.id) +
user.password + unicode(user.last_login) +
unicode(timestamp)).hexdigest()[::2]
return "%s-%s" % (ts_b36, hash)
def _num_days(self, dt):
return (dt - date(2001,1,1)).days
def _today(self):
# Used for mocking in tests
return date.today()
default_token_generator = PasswordResetTokenGenerator()

View File

@ -8,6 +8,9 @@ urlpatterns = patterns('',
('^logout/$', 'django.contrib.auth.views.logout'), ('^logout/$', 'django.contrib.auth.views.logout'),
('^password_change/$', 'django.contrib.auth.views.password_change'), ('^password_change/$', 'django.contrib.auth.views.password_change'),
('^password_change/done/$', 'django.contrib.auth.views.password_change_done'), ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
('^password_reset/$', 'django.contrib.auth.views.password_reset') ('^password_reset/$', 'django.contrib.auth.views.password_reset'),
('^password_reset/done/$', 'django.contrib.auth.views.password_reset_done'),
('^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm'),
('^reset/done/$', 'django.contrib.auth.views.password_reset_complete'),
) )

View File

@ -1,13 +1,14 @@
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm, AdminPasswordChangeForm from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm, AdminPasswordChangeForm
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import render_to_response, get_object_or_404 from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.sites.models import Site, RequestSite from django.contrib.sites.models import Site, RequestSite
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, Http404
from django.template import RequestContext from django.template import RequestContext
from django.utils.http import urlquote from django.utils.http import urlquote, base36_to_int
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -65,19 +66,29 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
login_url = settings.LOGIN_URL login_url = settings.LOGIN_URL
return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next))) return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next)))
# 4 views for password reset:
# - password_reset sends the mail
# - password_reset_done shows a success message for the above
# - password_reset_confirm checks the link the user clicked and
# prompts for a new password
# - password_reset_complete shows a success message for the above
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html', email_template_name='registration/password_reset_email.html',
password_reset_form=PasswordResetForm): password_reset_form=PasswordResetForm, token_generator=default_token_generator):
if request.method == "POST": if request.method == "POST":
form = password_reset_form(request.POST) form = password_reset_form(request.POST)
if form.is_valid(): if form.is_valid():
opts = {}
opts['use_https'] = request.is_secure()
opts['token_generator'] = token_generator
if is_admin_site: if is_admin_site:
form.save(domain_override=request.META['HTTP_HOST']) opts['domain_override'] = request.META['HTTP_HOST']
else: else:
if Site._meta.installed: opts['email_template_name'] = email_template_name
form.save(email_template_name=email_template_name) if not Site._meta.installed:
else: opts['domain_override'] = RequestSite(request).domain
form.save(domain_override=RequestSite(request).domain, email_template_name=email_template_name) form.save(**opts)
return HttpResponseRedirect('%sdone/' % request.path) return HttpResponseRedirect('%sdone/' % request.path)
else: else:
form = password_reset_form() form = password_reset_form()
@ -88,6 +99,39 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas
def password_reset_done(request, template_name='registration/password_reset_done.html'): def password_reset_done(request, template_name='registration/password_reset_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request)) return render_to_response(template_name, context_instance=RequestContext(request))
def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html',
token_generator=default_token_generator, set_password_form=SetPasswordForm):
"""
View that checks the hash in a password reset link and presents a
form for entering a new password.
"""
assert uidb36 is not None and token is not None # checked by URLconf
try:
uid_int = base36_to_int(uidb36)
except ValueError:
raise Http404
user = get_object_or_404(User, id=uid_int)
context_instance = RequestContext(request)
if token_generator.check_token(user, token):
context_instance['validlink'] = True
if request.method == 'POST':
form = set_password_form(user, request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect("../done/")
else:
form = set_password_form(None)
else:
context_instance['validlink'] = False
form = None
context_instance['form'] = form
return render_to_response(template_name, context_instance=context_instance)
def password_reset_complete(request, template_name='registration/password_reset_complete.html'):
return render_to_response(template_name, context_instance=RequestContext(request))
def password_change(request, template_name='registration/password_change_form.html'): def password_change(request, template_name='registration/password_change_form.html'):
if request.method == "POST": if request.method == "POST":
form = PasswordChangeForm(request.user, request.POST) form = PasswordChangeForm(request.user, request.POST)

View File

@ -65,3 +65,32 @@ def http_date(epoch_seconds=None):
""" """
rfcdate = formatdate(epoch_seconds) rfcdate = formatdate(epoch_seconds)
return '%s GMT' % rfcdate[:25] return '%s GMT' % rfcdate[:25]
# Base 36 functions: useful for generating compact URLs
def base36_to_int(s):
"""
Convertd a base 36 string to an integer
"""
return int(s, 36)
def int_to_base36(i):
"""
Converts an integer to a base36 string
"""
digits = "0123456789abcdefghijklmnopqrstuvwxyz"
factor = 0
# Find starting factor
while True:
factor += 1
if i < 36 ** factor:
factor -= 1
break
base36 = []
# Construct base36 representation
while factor >= 0:
j = 36 ** factor
base36.append(digits[i / j])
i = i % j
factor -= 1
return ''.join(base36)