From 4db38ebf4a5c0d3791669fb9845165f97f0f6fbf Mon Sep 17 00:00:00 2001 From: Dan Tehranian Date: Sun, 3 Jun 2012 17:39:45 -0700 Subject: [PATCH] upgrade to django 1.3; add ability to have custom URLs; add tests --- LICENSE | 1 + README | 7 +- deploy/deploy.wsgi | 20 --- manage.py | 7 +- settings.py | 76 ++++---- shortener/admin.py | 5 +- shortener/baseconv.py | 41 +++-- shortener/forms.py | 32 ++++ shortener/models.py | 48 +---- shortener/tests.py | 225 +++++++++++++++--------- shortener/urls.py | 10 ++ shortener/views.py | 118 ++++--------- templates/base.html | 74 ++------ templates/shortener/form.inc.html | 7 + templates/shortener/index.html | 28 ++- templates/shortener/submit_failed.html | 15 +- templates/shortener/submit_success.html | 26 +-- urls.py | 16 +- 18 files changed, 337 insertions(+), 419 deletions(-) delete mode 100644 deploy/deploy.wsgi mode change 100755 => 100644 shortener/baseconv.py create mode 100644 shortener/forms.py create mode 100644 shortener/urls.py create mode 100644 templates/shortener/form.inc.html diff --git a/LICENSE b/LICENSE index 550880b..e027f06 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright (c) 2009 Nilesh Kapadia +Copyright (c) 2012 Dan Tehranian Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/README b/README index 0848741..dcfcec9 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -url-shortener +django-url-shortener ============= This is URL shortening application using the Django framework @@ -10,11 +10,6 @@ main page shows the 10 most recent and 10 most popular URLs. Prerequisites ============= -Download Blueprint from: http://www.blueprintcss.org/ -Copy the "blueprint" folder into static/css/ (which you may need to create) - -Note that in a production installation, you'll want to have your web -server serve the "static" folder instead of letting Django serve it. Settings ======== diff --git a/deploy/deploy.wsgi b/deploy/deploy.wsgi deleted file mode 100644 index 24fdefa..0000000 --- a/deploy/deploy.wsgi +++ /dev/null @@ -1,20 +0,0 @@ -import os -import sys - -# redirect sys.stdout to sys.stderr for bad libraries like geopy that uses -# print statements for optional import exceptions. -sys.stdout = sys.stderr - -from os.path import abspath, dirname, join -from site import addsitedir - -PROJECT_ROOT = abspath(join(dirname(__file__), "../")) - -sys.path.insert(0, PROJECT_ROOT) -sys.path.insert(0, abspath(join(dirname(__file__), "../../"))) - -from django.core.handlers.wsgi import WSGIHandler - -os.environ["DJANGO_SETTINGS_MODULE"] = "urlweb.settings" - -application = WSGIHandler() diff --git a/manage.py b/manage.py index 5e78ea9..3e4eedc 100755 --- a/manage.py +++ b/manage.py @@ -1,11 +1,14 @@ #!/usr/bin/env python from django.core.management import execute_manager +import imp try: - import settings # Assumed to be in the same directory. + imp.find_module('settings') # Assumed to be in the same directory. except ImportError: import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) sys.exit(1) +import settings + if __name__ == "__main__": execute_manager(settings) diff --git a/settings.py b/settings.py index 96cfa87..6909a0b 100644 --- a/settings.py +++ b/settings.py @@ -1,15 +1,7 @@ -# Django settings for urlweb project. -import os, logging -#from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS +import os +import sys -logging.basicConfig( - level = logging.DEBUG, - format = '%(asctime)s %(levelname)s %(message)s', -) - -logging.debug("Reading settings...") - -PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) +SITE_ROOT = os.path.abspath(os.path.dirname(__file__)) DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -20,84 +12,84 @@ MANAGERS = ADMINS -DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. -#DATABASE_NAME = '' # Or path to database file if using sqlite3. -DATABASE_NAME = os.path.join(PROJECT_PATH, 'database.sqlite') +DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = 'db.sqlite' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Chicago' +TIME_ZONE = 'America/Los_Angeles' -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. USE_I18N = True +STATIC_ROOT = os.path.join(SITE_ROOT, 'static') +STATIC_URL = '/static/' + # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = '' +MEDIA_ROOT = os.path.join(SITE_ROOT, 'media') # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '' +MEDIA_URL = '/media/' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". ADMIN_MEDIA_PREFIX = '/media/' +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + #'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + # Make this unique, and don't share it with anybody. SECRET_KEY = '#### CHANGE_ME ####' -# List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', ) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', -# 'django.middleware.transaction.TransactionMiddleware', + #'django.middleware.csrf.CsrfViewMiddleware', ) -ROOT_URLCONF = 'urlweb.urls' +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.core.context_processors.debug', + 'django.core.context_processors.media', + 'django.core.context_processors.request', + 'django.core.context_processors.static', +) + +ROOT_URLCONF = 'urls' TEMPLATE_DIRS = ( - os.path.join(PROJECT_PATH, 'templates') + os.path.join(SITE_ROOT, 'templates') ) INSTALLED_APPS = ( + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.messages', 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.admin', - 'urlweb.shortener', + 'shortener', ) -STATIC_DOC_ROOT = os.path.join(PROJECT_PATH, 'static') LOGIN_REDIRECT_URL = '/' -#TEMPLATE_CONTEXT_PROCESSORS += ( -# 'django.core.context_processors.request', -# ) - SITE_NAME = 'localhost:8000' SITE_BASE_URL = 'http://' + SITE_NAME + '/' REQUIRE_LOGIN = True diff --git a/shortener/admin.py b/shortener/admin.py index 61b4ffb..468aa49 100644 --- a/shortener/admin.py +++ b/shortener/admin.py @@ -1,9 +1,8 @@ from django.contrib import admin -from urlweb.shortener.models import Link +from shortener.models import Link class LinkAdmin(admin.ModelAdmin): - model = Link - extra = 3 + pass admin.site.register(Link, LinkAdmin) diff --git a/shortener/baseconv.py b/shortener/baseconv.py old mode 100755 new mode 100644 index 5496d19..a4a0fc1 --- a/shortener/baseconv.py +++ b/shortener/baseconv.py @@ -11,19 +11,37 @@ >>> base20.to_decimal('31e') 1234 """ +import numbers +import string + + +class EncodingError(ValueError): + pass + + +class DecodingError(ValueError): + pass + class BaseConverter(object): - decimal_digits = "0123456789" - + decimal_digits = string.digits + def __init__(self, digits): self.digits = digits - + def from_decimal(self, i): + if not isinstance(i, numbers.Real): + raise EncodingError('%s is not an int()' % i) return self.convert(i, self.decimal_digits, self.digits) - + def to_decimal(self, s): + if not isinstance(s, basestring): + raise DecodingError('%s is not a basestring()' % s) + for char in s: + if char not in self.digits: + raise EncodingError('Invalid character for encoding: %s' % digit) 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] == '-': @@ -35,13 +53,13 @@ def convert(number, fromdigits, todigits): # make an integer out of the number x = 0 for digit in str(number): - x = x * len(fromdigits) + fromdigits.index(digit) - + x = x * len(fromdigits) + fromdigits.index(digit) + # create the result in base 'len(todigits)' if x == 0: res = todigits[0] else: - res = "" + res = '' while x > 0: digit = x % len(todigits) res = todigits[digit] + res @@ -51,8 +69,7 @@ def convert(number, fromdigits, todigits): return res convert = staticmethod(convert) + bin = BaseConverter('01') -hexconv = BaseConverter('0123456789ABCDEF') -base62 = BaseConverter( - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz' -) +hexconv = BaseConverter(string.hexdigits) +base62 = BaseConverter(string.digits + string.letters) diff --git a/shortener/forms.py b/shortener/forms.py new file mode 100644 index 0000000..664c24f --- /dev/null +++ b/shortener/forms.py @@ -0,0 +1,32 @@ +from django import forms + +from shortener.baseconv import base62 +from shortener.models import Link + +class LinkSubmitForm(forms.Form): + url = forms.URLField( + label='URL to be shortened',) + custom = forms.CharField( + label='Custom shortened name', + required=False,) + + def clean_custom(self): + custom = self.cleaned_data['custom'] + if not custom: + return + + # test for characters in the requested custom alias that are not + # available in our base62 enconding + for char in custom: + if char not in base62.digits: + raise forms.ValidationError('Invalid character: "%s"' % char) + # make sure this custom alias is not alrady taken + id = base62.to_decimal(custom) + try: + if Link.objects.filter(id=id).exists(): + raise forms.ValidationError('"%s" is already taken' % custom) + except OverflowError: + raise forms.ValidationError( + "Your custom name is too long. Are you sure you wanted a " + "shortening service? :)") + return custom diff --git a/shortener/models.py b/shortener/models.py index 0502d1b..611576d 100644 --- a/shortener/models.py +++ b/shortener/models.py @@ -2,59 +2,25 @@ from django.db import models from django.conf import settings -#from django.contrib.auth.models import User -from django import forms -from urlweb.shortener.baseconv import base62 +from shortener.baseconv import base62 class Link(models.Model): """ Model that represents a shortened URL - - # Initialize by deleting all Link objects - >>> Link.objects.all().delete() - - # Create some Link objects - >>> link1 = Link.objects.create(url="http://www.google.com/") - >>> link2 = Link.objects.create(url="http://www.nileshk.com/") - - # Get base 62 representation of id - >>> link1.to_base62() - 'B' - >>> link2.to_base62() - 'C' - - # Set SITE_BASE_URL to something specific - >>> settings.SITE_BASE_URL = 'http://uu4.us/' - - # Get short URL's - >>> link1.short_url() - 'http://uu4.us/B' - >>> link2.short_url() - 'http://uu4.us/C' - - # Test usage_count - >>> link1.usage_count - 0 - >>> link1.usage_count += 1 - >>> link1.usage_count - 1 - """ - url = models.URLField(verify_exists=True, unique=True) + url = models.URLField() date_submitted = models.DateTimeField(auto_now_add=True) - usage_count = models.IntegerField(default=0) + usage_count = models.PositiveIntegerField(default=0) def to_base62(self): return base62.from_decimal(self.id) def short_url(self): return settings.SITE_BASE_URL + self.to_base62() - + def __unicode__(self): - return self.to_base62() + ' : ' + self.url + return '%s : %s' % (self.to_base62(), self.url) -class LinkSubmitForm(forms.Form): - u = forms.URLField(verify_exists=True, - label='URL to be shortened:', - ) + class Meta: + get_latest_by = 'date_submitted' diff --git a/shortener/tests.py b/shortener/tests.py index 85e4920..1809392 100644 --- a/shortener/tests.py +++ b/shortener/tests.py @@ -1,89 +1,138 @@ -""" -Tests for views -""" - -__test__ = {"doctest": """ - -# Initialize by deleting all Link objects ->>> from models import Link ->>> Link.objects.all().delete() - ->>> from django.test import Client ->>> client = Client() - -# Index page ->>> r = client.get('/') ->>> r.status_code # / -200 ->>> r.template[0].name -'shortener/index.html' - -# Turn off logged-in requirement and set base URL ->>> from django.conf import settings ->>> settings.REQUIRE_LOGIN = False ->>> settings.SITE_BASE_URL = 'http://uu4.us/' - -# Empty submission should forward to error page ->>> r = client.get('/submit/') ->>> r.status_code # /submit/ -200 ->>> r.template[0].name # /submit/ -'shortener/submit_failed.html' - -# Submit a URL ->>> url = 'http://www.google.com/' ->>> r = client.get('/submit/', {'u': url}) ->>> r.status_code # /submit/u?=http%3A%2F%2Fwww.google.com%2F -200 ->>> r.template[0].name -'shortener/submit_success.html' ->>> link = r.context[0]['link'] ->>> link.to_base62() -'B' ->>> link.short_url() -'http://uu4.us/B' ->>> link_from_db = Link.objects.get(url = url) ->>> base62 = link_from_db.to_base62() ->>> base62 -'B' ->>> link_from_db.usage_count -0 - -# Short URL for previously submitted URL ->>> r = client.get('/' + base62) ->>> r.status_code # '/' + base62 -301 ->>> r['Location'] -'http://www.google.com/' - -# Invalid URL should get a 404 ->>> r = client.get('/INVALID') ->>> r.status_code # /INVALID -404 - -# Index now shows link in recent_links / most_popular_links ->>> r = client.get('/') ->>> r.status_code # / -200 ->>> r.template[0].name -'shortener/index.html' ->>> context = r.context[0] ->>> len(context['recent_links']) -1 ->>> len(context['most_popular_links']) -1 - -# Get info on Link ->>> r = client.get('/info/' + base62) ->>> r.status_code # info -200 ->>> r.template[0].name -'shortener/link_info.html' ->>> link = r.context[0]['link'] ->>> link.url -u'http://www.google.com/' ->>> link.usage_count # Usage count should be 1 now -1 - -"""} +import random +import string +import sys + +from django.core.urlresolvers import reverse +from django.test import TestCase + +from shortener.baseconv import base62, DecodingError, EncodingError +from shortener.models import Link + + +class ViewTestCase(TestCase): + def test_submit(self): + url = u'http://www.python.org/' + response = self.client.post(reverse('submit'), {'url': url,}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'shortener/submit_success.html') + self.assertIn('link', response.context) + link = response.context['link'] + self.assertIsInstance(link, Link) + self.assertEqual(url, link.url) + self.assertEqual(link.usage_count, 0) + self.assertEqual(base62.from_decimal(link.id), link.to_base62()) + + def test_submit_with_custom(self): + url = u'http://www.python.org/' + custom = 'mylink' + response = self.client.post(reverse('submit'), { + 'url': url, 'custom': custom}) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'shortener/submit_success.html') + self.assertIn('link', response.context) + link = response.context['link'] + self.assertIsInstance(link, Link) + self.assertEqual(url, link.url) + self.assertEqual(link.usage_count, 0) + self.assertEqual(link.to_base62(), custom) + + def test_submit_with_bad_character_in_custom(self): + """ + Submit with an invalid character in custom + """ + url = u'http://www.python.org/' + custom = 'my_link_bad_chars:##$#$%^$&%^**' + response = self.client.post(reverse('submit'), { + 'url': url, 'custom': custom}) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'shortener/submit_failed.html') + self.assertFormError(response, 'link_form', 'custom', 'Invalid character: "_"') + self.assertNotIn('link', response.context) + + def test_submit_with_custom_no_repeats(self): + url = u'http://www.python.org/' + custom = 'mylink' + + # first time should succeed + response = self.client.post(reverse('submit'), { + 'url': url, 'custom': custom}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'shortener/submit_success.html') + self.assertIn('link', response.context) + link = response.context['link'] + self.assertIsInstance(link, Link) + self.assertEqual(url, link.url) + self.assertEqual(link.usage_count, 0) + self.assertEqual(link.to_base62(), custom) + + # second time should be an error + response = self.client.post(reverse('submit'), { + 'url': url, 'custom': custom}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'shortener/submit_failed.html') + self.assertFormError(response, 'link_form', 'custom', '"%s" is already taken' % custom) + + self.assertNotIn('link', response.context) + + def test_follow(self): + url = u'http://www.python.org/' + response = self.client.post(reverse('submit'), {'url': url,}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'shortener/submit_success.html') + + link = response.context['link'] + self.assertIsInstance(link, Link) + self.assertEqual(url, link.url) + self.assertEqual(base62.from_decimal(link.id), link.to_base62()) + self.assertEqual(link.usage_count, 0) + + # follow the short url and get a redirect + response = self.client.get(link.short_url()) + self.assertRedirects(response, url, 301) + + # re-fetch link so that we can make sure that usage_count incremented + link = Link.objects.get(id=link.id) + self.assertEqual(link.usage_count, 1) + + +class LinkTestCase(TestCase): + def test_create(self): + link = Link.objects.create(url='http://www.python.org') + + # verify that link.short_url() is derived from link.id and that + # the short_url() ends with the base_62() encoding of link.id + self.assertEqual(link.to_base62(), base62.from_decimal(link.id)) + self.assertTrue(link.short_url().endswith(link.to_base62())) + + def test_create_with_custom_id(self): + """ + Create a shortened URL with non-default id specified + """ + id = 5000 + link = Link.objects.create(id=id, url='http://www.python.org') + self.assertEqual(link.to_base62(), base62.from_decimal(id)) + self.assertTrue(link.short_url().endswith(link.to_base62())) + + +class BaseconvTestCase(TestCase): + def test_symmetry_int(self): + random_int = random.randint(0, sys.maxint) + encoded_int = base62.from_decimal(random_int) + self.assertEqual(random_int, base62.to_decimal(encoded_int)) + + def test_encoding_non_int_fails(self): + try: + encoding = base62.from_decimal(string.letters) + except EncodingError, e: + err = e + self.assertIsInstance(err, EncodingError) + + def test_decoding_non_str_fails(self): + try: + decoding = base62.to_decimal(sys.maxint) + except DecodingError, e: + err = e + self.assertIsInstance(err, DecodingError) diff --git a/shortener/urls.py b/shortener/urls.py new file mode 100644 index 0000000..1a5f54a --- /dev/null +++ b/shortener/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import patterns, url + + +urlpatterns = patterns('shortener.views', + url(r'^$', 'index', name='index'), + url(r'^info/(?P\w+)$', 'info', name='info'), + url(r'^submit/$', 'submit', name='submit'), + url(r'^(?P\w+)$', 'follow', name='follow'), +) + diff --git a/shortener/views.py b/shortener/views.py index 416c1c8..c9f06a3 100644 --- a/shortener/views.py +++ b/shortener/views.py @@ -1,108 +1,60 @@ -import logging +from django.db.models import F +from django.http import HttpResponsePermanentRedirect +from django.shortcuts import get_object_or_404, render +from django.views.decorators.http import require_GET, require_POST -from django.contrib.auth.decorators import login_required -from django.contrib.auth import authenticate -from django.views.generic import list_detail -from django.shortcuts import get_object_or_404, get_list_or_404, render_to_response -from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponsePermanentRedirect -from django.utils import simplejson -from django.template import RequestContext -from django.views.decorators.http import require_POST -from django.db import transaction -from django.conf import settings +from shortener.baseconv import base62 +from shortener.models import Link +from shortener.forms import LinkSubmitForm -from urlweb.shortener.baseconv import base62 -from urlweb.shortener.models import Link, LinkSubmitForm +@require_GET def follow(request, base62_id): - """ + """ View which gets the link for the given base62_id value and redirects to it. """ - key = base62.to_decimal(base62_id) - link = get_object_or_404(Link, pk = key) - link.usage_count += 1 + link = get_object_or_404(Link, id=base62.to_decimal(base62_id)) + link.usage_count = F('usage_count') + 1 link.save() return HttpResponsePermanentRedirect(link.url) -def default_values(request, link_form=None): - """ - Return a new object with the default values that are typically - returned in a request. - """ - if not link_form: - link_form = LinkSubmitForm() - allowed_to_submit = is_allowed_to_submit(request) - return { 'show_bookmarklet': allowed_to_submit, - 'show_url_form': allowed_to_submit, - 'site_name': settings.SITE_NAME, - 'site_base_url': settings.SITE_BASE_URL, - 'link_form': link_form, - } +@require_GET def info(request, base62_id): """ View which shows information on a particular link """ - key = base62.to_decimal(base62_id) - link = get_object_or_404(Link, pk = key) - values = default_values(request) - values['link'] = link - return render_to_response( - 'shortener/link_info.html', - values, - context_instance=RequestContext(request)) + link = get_object_or_404(Link, id=base62.to_decimal(base62_id)) + return render(request, 'shortener/link_info.html', {'link': link,}) + +@require_POST def submit(request): """ View for submitting a URL """ - if settings.REQUIRE_LOGIN and not request.user.is_authenticated(): - # TODO redirect to an error page - raise Http404 - url = None - link_form = None - if request.GET: - link_form = LinkSubmitForm(request.GET) - elif request.POST: - link_form = LinkSubmitForm(request.POST) - if link_form and link_form.is_valid(): - url = link_form.cleaned_data['u'] - link = None - try: - link = Link.objects.get(url = url) - except Link.DoesNotExist: - pass - if link == None: - new_link = Link(url = url) - new_link.save() - link = new_link - values = default_values(request) - values['link'] = link - return render_to_response( - 'shortener/submit_success.html', - values, - context_instance=RequestContext(request)) - values = default_values(request, link_form=link_form) - return render_to_response( - 'shortener/submit_failed.html', - values, - context_instance=RequestContext(request)) + form = LinkSubmitForm(request.POST) + if form.is_valid(): + kwargs = {'url': form.cleaned_data['url']} + custom = form.cleaned_data['custom'] + if custom: + # specify an explicit id corresponding to the custom url + kwargs.update({'id': base62.to_decimal(custom)}) + link = Link.objects.create(**kwargs) + return render(request, 'shortener/submit_success.html', {'link': link}) + else: + return render(request, 'shortener/submit_failed.html', {'link_form': form}) + +@require_GET def index(request): """ - View for main page (lists recent and popular links) + View for main page """ - values = default_values(request) - values['recent_links'] = Link.objects.all().order_by('-date_submitted')[0:10] - values['most_popular_links'] = Link.objects.all().order_by('-usage_count')[0:10] - return render_to_response( - 'shortener/index.html', - values, - context_instance=RequestContext(request)) + values = { + 'link_form': LinkSubmitForm(), + 'recent_links': Link.objects.all().order_by('-date_submitted')[:5], + 'most_popular_links': Link.objects.all().order_by('-usage_count')[:5],} + return render(request, 'shortener/index.html', values) -def is_allowed_to_submit(request): - """ - Return true if user is allowed to submit URLs - """ - return not settings.REQUIRE_LOGIN or request.user.is_authenticated() diff --git a/templates/base.html b/templates/base.html index 60bdc5d..20cdfb6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,65 +1,23 @@ - - + - {% block title %}URLs{% endblock %} - - - - - - - - - - - - {% block extra_head %} - {% endblock %} + {% block title %}django-url-shortener{% endblock %} + {% block extra_head %}{% endblock %} -
- -
- {% block content %}{% endblock %} -
-
+

{{ request.META.HTTP_HOST }}

+
+ {% block content %}{% endblock %} + + {% block url_form %} + {% include 'shortener/form.inc.html' %} + {% endblock %} + {% block bookmarklet %} +
+ Bookmarklet (drag this to your bookmarks bar):
+ Shorten with {{ request.META.HTTP_HOST }} + {% endblock %} -
- {% if show_url_form %} - {% block url_form %} -
- {{ link_form.as_p }} - -
- {% endblock %} - {% endif %} -
-
- {% if show_bookmarklet %} - {% block bookmarklet %} -
- Bookmarklet (drag this to your bookmarks bar):
- Shorten with {{ site_name }} - {% endblock %} - {% endif %} -
- {% block extra_body %}{% endblock %} -
+ {% block extra_body %}{% endblock %} diff --git a/templates/shortener/form.inc.html b/templates/shortener/form.inc.html new file mode 100644 index 0000000..959dda9 --- /dev/null +++ b/templates/shortener/form.inc.html @@ -0,0 +1,7 @@ +{% if link_form %} +
+ {% csrf_token %} + {{ link_form.as_p }} + +
+{% endif %} diff --git a/templates/shortener/index.html b/templates/shortener/index.html index c1bae4c..082d5f3 100644 --- a/templates/shortener/index.html +++ b/templates/shortener/index.html @@ -1,27 +1,19 @@ {% extends "base.html" %} + {% block content %}

Recent Links

    -{% for link in recent_links %} -
  • {{ link.url }} (Score: {{ link.usage_count }}) -(Info)
  • -{% endfor %} + {% for link in recent_links %} +
  • {{ link.url }} (Count: {{ link.usage_count }}) + (Info)
  • + {% endfor %}

Most Popular Links

    -{% for link in most_popular_links %} -
  • {{ link.url }} (Score: {{ link.usage_count }}) -(Info)
  • -{% endfor %} + {% for link in most_popular_links %} +
  • {{ link.url }} (Count: {{ link.usage_count }}) + (Info)
  • + {% endfor %}
-{% endblock %} - -{% block extra_body %} - -{% endblock %} +{% endblock content %} diff --git a/templates/shortener/submit_failed.html b/templates/shortener/submit_failed.html index b565c6e..d10301c 100644 --- a/templates/shortener/submit_failed.html +++ b/templates/shortener/submit_failed.html @@ -1,16 +1,5 @@ {% extends "base.html" %} -{% block title %}URL submission failed{% endblock %} - {% block content %} -

URL submission failed

-{% endblock %} - -{% block extra_body %} - -{% endblock %} +

URL submission failed

+{% endblock content %} diff --git a/templates/shortener/submit_success.html b/templates/shortener/submit_success.html index 92e1d05..04ab93f 100644 --- a/templates/shortener/submit_success.html +++ b/templates/shortener/submit_success.html @@ -1,29 +1,9 @@ {% extends "base.html" %} -{% block title %}URL shortened{% endblock %} - {% block content %} The following URL:
-
-{{ link.url }}
-
+
{{ link.url }}

Was shortened to:
- -
-
-Score: {{ link.usage_count }} -(Info) -
-
-{% endblock %} - -{% block extra_body %} - -{% endblock %} + +{% endblock content %} diff --git a/urls.py b/urls.py index 272634a..693bd6a 100644 --- a/urls.py +++ b/urls.py @@ -1,17 +1,13 @@ -from django.conf.urls.defaults import * +from django.conf.urls.defaults import include, patterns, url from django.contrib import admin from django.conf import settings admin.autodiscover() -urlpatterns = patterns( - '', - (r'^$', 'shortener.views.index'), - (r'^admin/(.*)', admin.site.root), - (r'^submit/$', 'shortener.views.submit'), - (r'^(?P\w+)$', 'shortener.views.follow'), - (r'^info/(?P\w+)$', 'shortener.views.info'), +urlpatterns = patterns('', + url(r'^admin/', include(admin.site.urls)), + url(r'', include('shortener.urls')), - (r'^static/(?P.*)$', 'django.views.static.serve', - {'document_root': settings.STATIC_DOC_ROOT}), + url(r'^static/(?P.*)$', 'django.views.static.serve', + {'document_root': settings.STATIC_ROOT}), )