aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJacob Kaplan-Moss2009-04-12 16:25:05 -0500
committerJacob Kaplan-Moss2009-04-12 16:25:05 -0500
commitb5ee253b28673fcfad49f09d9d2687e86ed520b7 (patch)
tree32f3f5939bf8d69503f443573414ea80567d095e /src
parent4391a39ee4cde31ff9c9a56cd8d452b934136b4e (diff)
downloaddjango-shorturls-b5ee253b28673fcfad49f09d9d2687e86ed520b7.tar.bz2
Added a working shorturl redirect view, with tests.
Diffstat (limited to 'src')
-rw-r--r--src/shorturls/baseconv.py65
-rw-r--r--src/shorturls/fixtures/shorturls-test-data.json28
-rw-r--r--src/shorturls/tests/__init__.py3
-rw-r--r--src/shorturls/tests/models.py37
-rw-r--r--src/shorturls/tests/templates/404.html27
-rw-r--r--src/shorturls/tests/test_views.py61
-rw-r--r--src/shorturls/testsettings.py7
-rw-r--r--src/shorturls/urls.py9
-rw-r--r--src/shorturls/views.py53
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