aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Firshman2014-09-22 02:18:09 +0200
committerBen Firshman2014-09-22 02:18:09 +0200
commit47dcc785c2d6bcbbcf0cbaabdd5d8c825bc97e9c (patch)
treef184d0ff8da4d2644ae4faed3bddcb9e5790b596
parent967f4ddab7bb68bc1cbd10a30658c65fd67c9acb (diff)
parentab5312e7c035e76521140382b47113bcf50acbcf (diff)
downloaddjango-shorturls-47dcc785c2d6bcbbcf0cbaabdd5d8c825bc97e9c.tar.bz2
Merge pull request #8 from bfirsh/crockford-base32
Add Crockford's base32 and performance improvements
-rw-r--r--README.md13
-rw-r--r--src/shorturls/__init__.py19
-rw-r--r--src/shorturls/baseconv.py86
-rw-r--r--src/shorturls/templatetags/shorturl.py4
-rw-r--r--src/shorturls/tests/test_baseconv.py17
-rw-r--r--src/shorturls/tests/test_views.py1
-rw-r--r--src/shorturls/views.py25
7 files changed, 121 insertions, 44 deletions
diff --git a/README.md b/README.md
index 797bd78..f933880 100644
--- a/README.md
+++ b/README.md
@@ -87,3 +87,16 @@ differ.
If not defined, the redirect view will try to guess the proper domain by
consulting the ``django.contrib.sites`` framework, if installed, or the
requested domain, if not.
+
+#### ``SHORTURLS_DEFAULT_CONVERTER``
+
+The converter that is used to translate between short URLs and model IDs.
+Defaults to the built in base 62 conversion.
+
+Available converters:
+
+ - `shorturls.baseconv.base62` Base 62 encoding.
+ - `shorturls.baseconv.base32` [Douglas Crockford's base 32.](http://www.crockford.com/wrmg/base32.html)
+ - `shorturls.baseconv.hexconv` Hex encoding.
+ - `shorturls.baseconv.bin` Binary encoding, because why not.
+
diff --git a/src/shorturls/__init__.py b/src/shorturls/__init__.py
index e69de29..528e838 100644
--- a/src/shorturls/__init__.py
+++ b/src/shorturls/__init__.py
@@ -0,0 +1,19 @@
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+from shorturls import baseconv
+
+default_converter = baseconv.base62
+
+if hasattr(settings, 'SHORTURLS_DEFAULT_CONVERTER'):
+ mod_name, conv_name = settings.SHORTURLS_DEFAULT_CONVERTER.rsplit('.', 1)
+ try:
+ mod = import_module(mod_name)
+ except ImportError, e:
+ raise ImproperlyConfigured('Could not load converter specified by SHORTURLS_DEFAULT_CONVERTER. Error was: %s' % e)
+ try:
+ default_converter = getattr(mod, conv_name)
+ except AttributeError:
+ raise ImproperlyConfigured('Could not load converter specified by SHORTURLS_DEFAULT_CONVERTER. %s is not in %s.' % (conv_name, mod))
+
+ \ No newline at end of file
diff --git a/src/shorturls/baseconv.py b/src/shorturls/baseconv.py
index 1e22ed1..1b4fd15 100644
--- a/src/shorturls/baseconv.py
+++ b/src/shorturls/baseconv.py
@@ -13,46 +13,74 @@ Sample usage:
"""
class BaseConverter(object):
- decimal_digits = "0123456789"
+ decode_mapping = {}
def __init__(self, digits):
self.digits = digits
+ self.length = len(digits)
def from_decimal(self, i):
- return self.convert(i, self.decimal_digits, self.digits)
+ if i < 0:
+ i, neg = -i, 1
+ else:
+ neg = 0
+ enc = ''
+ while i >= self.length:
+ i, mod = divmod(i, self.length)
+ enc = self.digits[mod] + enc
+ enc = self.digits[i] + enc
+ if neg:
+ enc = '-' + enc
+ return enc
def to_decimal(self, s):
- return int(self.convert(s, self.digits, self.decimal_digits))
-
- def convert(number, fromdigits, todigits):
- # Based on http://code.activestate.com/recipes/111286/
- if str(number)[0] == '-':
- number = str(number)[1:]
- neg = 1
+ if self.decode_mapping:
+ new = ''
+ for digit in s:
+ if digit in self.decode_mapping:
+ new += self.decode_mapping[digit]
+ else:
+ new += digit
+ s = new
+ if str(s)[0] == '-':
+ s, neg = str(s)[1:], 1
else:
neg = 0
-
- # make an integer out of the number
- x = 0
- for digit in str(number):
- x = x * len(fromdigits) + fromdigits.index(digit)
+ decoded = 0
+ multi = 1
+ while len(s) > 0:
+ decoded += multi * self.digits.index(s[-1:])
+ multi = multi * self.length
+ s = s[:-1]
+ if neg:
+ decoded = -decoded
+ return decoded
- # create the result in base 'len(todigits)'
- if x == 0:
- res = todigits[0]
- else:
- res = ""
- while x > 0:
- digit = x % len(todigits)
- res = todigits[digit] + res
- x = int(x / len(todigits))
- if neg:
- res = '-' + res
- return res
- convert = staticmethod(convert)
-
bin = BaseConverter('01')
hexconv = BaseConverter('0123456789ABCDEF')
base62 = BaseConverter(
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'
-) \ No newline at end of file
+)
+
+class Base32Converter(BaseConverter):
+ """
+ http://www.crockford.com/wrmg/base32.html
+ """
+ decode_mapping = {
+ 'o': '0',
+ 'i': '1',
+ 'l': '1',
+ }
+
+ def __init__(self):
+ super(Base32Converter, self).__init__('0123456789abcdefghjkmnpqrstvwxyz')
+
+ def to_decimal(self, s):
+ return super(Base32Converter, self).to_decimal(s.lower())
+
+base32 = Base32Converter()
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
+
diff --git a/src/shorturls/templatetags/shorturl.py b/src/shorturls/templatetags/shorturl.py
index e1781cc..4c98d87 100644
--- a/src/shorturls/templatetags/shorturl.py
+++ b/src/shorturls/templatetags/shorturl.py
@@ -3,7 +3,7 @@ from django import template
from django.conf import settings
from django.core import urlresolvers
from django.utils.safestring import mark_safe
-from shorturls.baseconv import base62
+from shorturls import default_converter as converter
class ShortURL(template.Node):
@classmethod
@@ -27,7 +27,7 @@ class ShortURL(template.Node):
except (AttributeError, KeyError):
return ''
- tinyid = base62.from_decimal(obj.pk)
+ tinyid = converter.from_decimal(obj.pk)
if hasattr(settings, 'SHORT_BASE_URL') and settings.SHORT_BASE_URL:
return urlparse.urljoin(settings.SHORT_BASE_URL, prefix+tinyid)
diff --git a/src/shorturls/tests/test_baseconv.py b/src/shorturls/tests/test_baseconv.py
index 5e203b7..24f944f 100644
--- a/src/shorturls/tests/test_baseconv.py
+++ b/src/shorturls/tests/test_baseconv.py
@@ -16,4 +16,19 @@ class BaseConvTests(unittest.TestCase):
self._test_converter(baseconv.hexconv)
def test_base62(self):
- self._test_converter(baseconv.base62) \ No newline at end of file
+ self._test_converter(baseconv.base62)
+
+ def test_base32(self):
+ self._test_converter(baseconv.base32)
+
+ def test_base32_mappings(self):
+ converter = baseconv.base32
+ self.assertEqual(
+ converter.from_decimal(converter.to_decimal('Fo9')),
+ 'f09'
+ )
+ self.assertEqual(
+ converter.from_decimal(converter.to_decimal('43FG5i2L')),
+ '43fg5121'
+ )
+
diff --git a/src/shorturls/tests/test_views.py b/src/shorturls/tests/test_views.py
index a2813f2..e2c7b63 100644
--- a/src/shorturls/tests/test_views.py
+++ b/src/shorturls/tests/test_views.py
@@ -1,5 +1,4 @@
from django.conf import settings
-from django.http import Http404
from django.test import TestCase
from shorturls.baseconv import base62
diff --git a/src/shorturls/views.py b/src/shorturls/views.py
index d811fde..2d0a47b 100644
--- a/src/shorturls/views.py
+++ b/src/shorturls/views.py
@@ -4,9 +4,9 @@ from django.contrib.sites.models import Site, RequestSite
from django.db import models
from django.http import HttpResponsePermanentRedirect, Http404
from django.shortcuts import get_object_or_404
-from shorturls.baseconv import base62
+from shorturls import default_converter
-def redirect(request, prefix, tiny):
+def redirect(request, prefix, tiny, converter=default_converter):
"""
Redirect to a given object from a short URL.
"""
@@ -16,15 +16,18 @@ def redirect(request, prefix, tiny):
# any of that stuff goes wrong.
try:
app_label, model_name = settings.SHORTEN_MODELS[prefix].split('.')
- try:
- model = models.get_model(app_label, model_name)
- except LookupError:
- raise Http404('Bad model.')
- if not model:
- raise ValueError
- id = base62.to_decimal(tiny)
- except (AttributeError, ValueError, KeyError):
- raise Http404('Bad prefix, model, SHORTEN_MODELS, or encoded ID.')
+ except KeyError:
+ raise Http404('Bad prefix.')
+ try:
+ model = models.get_model(app_label, model_name)
+ except LookupError:
+ model = False
+ if not model:
+ raise Http404('Bad model specified in SHORTEN_MODELS.')
+ try:
+ id = converter.to_decimal(tiny)
+ except ValueError:
+ raise Http404('Bad encoded ID.')
# Try to look up the object. If it's not a valid object, or if it doesn't
# have an absolute url, bail again.