diff options
| author | Jacob Kaplan-Moss | 2009-04-12 16:25:05 -0500 |
|---|---|---|
| committer | Jacob Kaplan-Moss | 2009-04-12 16:25:05 -0500 |
| commit | b5ee253b28673fcfad49f09d9d2687e86ed520b7 (patch) | |
| tree | 32f3f5939bf8d69503f443573414ea80567d095e /src | |
| parent | 4391a39ee4cde31ff9c9a56cd8d452b934136b4e (diff) | |
| download | django-shorturls-b5ee253b28673fcfad49f09d9d2687e86ed520b7.tar.bz2 | |
Added a working shorturl redirect view, with tests.
Diffstat (limited to 'src')
| -rw-r--r-- | src/shorturls/baseconv.py | 65 | ||||
| -rw-r--r-- | src/shorturls/fixtures/shorturls-test-data.json | 28 | ||||
| -rw-r--r-- | src/shorturls/tests/__init__.py | 3 | ||||
| -rw-r--r-- | src/shorturls/tests/models.py | 37 | ||||
| -rw-r--r-- | src/shorturls/tests/templates/404.html | 27 | ||||
| -rw-r--r-- | src/shorturls/tests/test_views.py | 61 | ||||
| -rw-r--r-- | src/shorturls/testsettings.py | 7 | ||||
| -rw-r--r-- | src/shorturls/urls.py | 9 | ||||
| -rw-r--r-- | src/shorturls/views.py | 53 |
9 files changed, 285 insertions, 5 deletions
diff --git a/src/shorturls/baseconv.py b/src/shorturls/baseconv.py new file mode 100644 index 0000000..9012441 --- /dev/null +++ b/src/shorturls/baseconv.py @@ -0,0 +1,65 @@ +""" +Convert numbers from base 10 integers to base X strings and back again. + +Original: http://www.djangosnippets.org/snippets/1431/ + +Sample usage: + +>>> base20 = BaseConverter('0123456789abcdefghij') +>>> base20.from_decimal(1234) +'31e' +>>> base20.from_decimal('31e') +1234 +""" + +class BaseConverter(object): + decimal_digits = "0123456789" + + def __init__(self, digits): + self.digits = digits + + def from_decimal(self, i): + return self.convert(i, self.decimal_digits, self.digits) + + 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 + else: + neg = 0 + + # make an integer out of the number + x = 0 + for digit in str(number): + x = x * len(fromdigits) + fromdigits.index(digit) + + # 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' +) + +if __name__ == '__main__': + nums = [-10 ** 10, 10 ** 10] + range(-100, 100) + for convertor in [bin, hex, base62]: + for i in nums: + assert i == bin.to_decimal(bin.from_decimal(i)), '%s failed' % i + diff --git a/src/shorturls/fixtures/shorturls-test-data.json b/src/shorturls/fixtures/shorturls-test-data.json new file mode 100644 index 0000000..aa3c955 --- /dev/null +++ b/src/shorturls/fixtures/shorturls-test-data.json @@ -0,0 +1,28 @@ +[{ + "model": "shorturls.animal", + "pk": 12345, + "fields": { + "name": "Bear" + } +}, +{ + "model": "shorturls.animal", + "pk": 54321, + "fields": { + "name": "Frog" + } +}, +{ + "model": "shorturls.vegetable", + "pk": 785, + "fields": { + "name": "eggplant" + } +}, +{ + "model": "shorturls.mineral", + "pk": 10101, + "fields": { + "name": "granite" + } +}]
\ No newline at end of file diff --git a/src/shorturls/tests/__init__.py b/src/shorturls/tests/__init__.py index d983d15..6b7a7a3 100644 --- a/src/shorturls/tests/__init__.py +++ b/src/shorturls/tests/__init__.py @@ -1 +1,2 @@ -from test_views import *
\ No newline at end of file +from shorturls.tests.models import * +from shorturls.tests.test_views import *
\ No newline at end of file diff --git a/src/shorturls/tests/models.py b/src/shorturls/tests/models.py index f521e56..5416b17 100644 --- a/src/shorturls/tests/models.py +++ b/src/shorturls/tests/models.py @@ -1,3 +1,38 @@ """ A handful of test modules to test out resolving redirects. -"""
\ No newline at end of file +""" + +from django.db import models + +class Animal(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = 'shorturls' + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return '/animal/%s/' % self.id + +class Vegetable(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = 'shorturls' + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return 'http://example.net/veggies/%s' % self.id + +class Mineral(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = 'shorturls' + + def __unicode__(self): + return self.name
\ No newline at end of file diff --git a/src/shorturls/tests/templates/404.html b/src/shorturls/tests/templates/404.html new file mode 100644 index 0000000..e63d741 --- /dev/null +++ b/src/shorturls/tests/templates/404.html @@ -0,0 +1,27 @@ + ## + #### + ### ## + ### ## +# ### ## +################## +#################### +# ### + + ####### + ############### + ################## + # ## +# # + # ## + #### #### + ############### + ####### + + ## + #### + ### ## + ### ## +# ### ## +################## +#################### +# ###
\ No newline at end of file diff --git a/src/shorturls/tests/test_views.py b/src/shorturls/tests/test_views.py index 8fa11a4..a2813f2 100644 --- a/src/shorturls/tests/test_views.py +++ b/src/shorturls/tests/test_views.py @@ -1,5 +1,62 @@ +from django.conf import settings +from django.http import Http404 from django.test import TestCase +from shorturls.baseconv import base62 class RedirectViewTestCase(TestCase): - def test_this_is_working(self): - self.assertEquals(1, 1)
\ No newline at end of file + urls = 'shorturls.urls' + fixtures = ['shorturls-test-data.json'] + + def setUp(self): + self.old_shorten = getattr(settings, 'SHORTEN_MODELS', None) + self.old_base = getattr(settings, 'SHORTEN_FULL_BASE_URL', None) + settings.SHORTEN_MODELS = { + 'A': 'shorturls.animal', + 'V': 'shorturls.vegetable', + 'M': 'shorturls.mineral', + 'bad': 'not.amodel', + 'bad2': 'not.even.valid', + } + settings.SHORTEN_FULL_BASE_URL = 'http://example.com' + + def tearDown(self): + if self.old_shorten is not None: + settings.SHORTEN_MODELS = self.old_shorten + if self.old_base is not None: + settings.SHORTEN_FULL_BASE_URL = self.old_base + + def test_redirect(self): + """ + Test the basic operation of a working redirect. + """ + response = self.client.get('/A%s' % enc(12345)) + self.assertEqual(response.status_code, 301) + self.assertEqual(response['Location'], 'http://example.com/animal/12345/') + + def test_redirect_from_request(self): + """ + Test a relative redirect when the Sites app isn't installed. + """ + settings.SHORTEN_FULL_BASE_URL = None + response = self.client.get('/A%s' % enc(54321), HTTP_HOST='example.org') + self.assertEqual(response.status_code, 301) + self.assertEqual(response['Location'], 'http://example.org/animal/54321/') + + def test_redirect_complete_url(self): + """ + Test a redirect when the object returns a complete URL. + """ + response = self.client.get('/V%s' % enc(785)) + self.assertEqual(response.status_code, 301) + self.assertEqual(response['Location'], 'http://example.net/veggies/785') + + def test_bad_short_urls(self): + self.assertEqual(404, self.client.get('/badabcd').status_code) + self.assertEqual(404, self.client.get('/bad2abcd').status_code) + self.assertEqual(404, self.client.get('/Vssssss').status_code) + + def test_model_without_get_absolute_url(self): + self.assertEqual(404, self.client.get('/M%s' % enc(10101)).status_code) + +def enc(id): + return base62.from_decimal(id) diff --git a/src/shorturls/testsettings.py b/src/shorturls/testsettings.py index 5ce99da..2098abf 100644 --- a/src/shorturls/testsettings.py +++ b/src/shorturls/testsettings.py @@ -1,3 +1,8 @@ +import os + +DEBUG = TEMPLATE_DEBUG = True DATABASE_ENGINE = 'sqlite3' DATABASE_NAME = '/tmp/shorturls.db' -INSTALLED_APPS = ['shorturls']
\ No newline at end of file +INSTALLED_APPS = ['shorturls'] +ROOT_URLCONF = ['shorturls.urls'] +TEMPLATE_DIRS = os.path.join(os.path.dirname(__file__), 'tests', 'templates')
\ No newline at end of file diff --git a/src/shorturls/urls.py b/src/shorturls/urls.py new file mode 100644 index 0000000..b81e24d --- /dev/null +++ b/src/shorturls/urls.py @@ -0,0 +1,9 @@ +from django.conf import settings +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + url( + regex = '^(?P<prefix>%s)(?P<tiny>\w+)$' % '|'.join(settings.SHORTEN_MODELS.keys()), + view = 'shorturls.views.redirect', + ), +)
\ No newline at end of file diff --git a/src/shorturls/views.py b/src/shorturls/views.py new file mode 100644 index 0000000..d56ee1f --- /dev/null +++ b/src/shorturls/views.py @@ -0,0 +1,53 @@ +import urlparse +from django.conf import settings +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 + +def redirect(request, prefix, tiny): + """ + Redirect to a given object from a short URL. + """ + # Resolve the prefix and encoded ID into a model object and decoded ID. + # Many things here could go wrong -- bad prefix, bad value in + # SHORTEN_MODELS, no such model, bad encoding -- so just return a 404 if + # any of that stuff goes wrong. + try: + app_label, model_name = settings.SHORTEN_MODELS[prefix].split('.') + model = models.get_model(app_label, model_name) + if not model: raise ValueError + id = base62.to_decimal(tiny) + except (AttributeError, ValueError, KeyError): + raise Http404('Bad prefix, model, SHORTEN_MODELS, or 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. + obj = get_object_or_404(model, pk=id) + try: + url = obj.get_absolute_url() + except AttributeError: + raise Http404("'%s' models don't have a get_absolute_url() method." % model.__name__) + + # We might have to translate the URL -- the badly-named get_absolute_url + # actually returns a domain-relative URL -- into a fully qualified one. + + # If we got a fully-qualified URL, sweet. + if urlparse.urlsplit(url).scheme: + return HttpResponsePermanentRedirect(url) + + # Otherwise, we need to make a full URL by prepending a base URL. + # First, look for an explicit setting. + if hasattr(settings, 'SHORTEN_FULL_BASE_URL') and settings.SHORTEN_FULL_BASE_URL: + base = settings.SHORTEN_FULL_BASE_URL + + # Next, if the sites app is enabled, redirect to the current site. + elif Site._meta.installed: + base = 'http://%s/' % Site.objects.get_current().domain + + # Finally, fall back on the current request. + else: + base = 'http://%s/' % RequestSite(request).domain + + return HttpResponsePermanentRedirect(urlparse.urljoin(base, url))
\ No newline at end of file |
