diff options
| -rw-r--r-- | README.md | 13 | ||||
| -rw-r--r-- | src/shorturls/__init__.py | 19 | ||||
| -rw-r--r-- | src/shorturls/baseconv.py | 86 | ||||
| -rw-r--r-- | src/shorturls/templatetags/shorturl.py | 4 | ||||
| -rw-r--r-- | src/shorturls/tests/test_baseconv.py | 17 | ||||
| -rw-r--r-- | src/shorturls/tests/test_views.py | 1 | ||||
| -rw-r--r-- | src/shorturls/views.py | 25 | 
7 files changed, 121 insertions, 44 deletions
| @@ -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. | 
