diff options
| author | Vladimir Gorej | 2012-05-27 20:14:17 +0200 |
|---|---|---|
| committer | Vladimir Gorej | 2012-05-27 20:14:17 +0200 |
| commit | 4bd7b72aa0ca3a09ab99e1a3b41753df81de6af9 (patch) | |
| tree | 6328ed9894666eefc1f0d6b6c7b28a87b1fb4d31 | |
| download | django-brevisurl-4bd7b72aa0ca3a09ab99e1a3b41753df81de6af9.tar.bz2 | |
Initial commit
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | LICENSE | 26 | ||||
| -rw-r--r-- | README.rst | 0 | ||||
| -rw-r--r-- | brevisurl/__init__.py | 37 | ||||
| -rw-r--r-- | brevisurl/backends/__init__.py | 0 | ||||
| -rw-r--r-- | brevisurl/backends/base.py | 41 | ||||
| -rw-r--r-- | brevisurl/backends/local.py | 53 | ||||
| -rw-r--r-- | brevisurl/models.py | 58 | ||||
| -rw-r--r-- | brevisurl/settings.py | 3 | ||||
| -rw-r--r-- | brevisurl/templatetags/__init__.py | 0 | ||||
| -rw-r--r-- | brevisurl/templatetags/brevisurltags.py | 11 | ||||
| -rw-r--r-- | brevisurl/tests/__init__.py | 6 | ||||
| -rw-r--r-- | brevisurl/tests/backends/__init__.py | 1 | ||||
| -rw-r--r-- | brevisurl/tests/backends/test_local.py | 55 | ||||
| -rw-r--r-- | brevisurl/tests/templatetags/__init__.py | 0 | ||||
| -rw-r--r-- | brevisurl/tests/templatetags/test_brevisurltags.py | 27 | ||||
| -rw-r--r-- | brevisurl/tests/test_brevisurl.py | 22 | ||||
| -rw-r--r-- | brevisurl/tests/test_models.py | 38 | ||||
| -rw-r--r-- | brevisurl/tests/test_utils.py | 22 | ||||
| -rw-r--r-- | brevisurl/tests/test_views.py | 28 | ||||
| -rw-r--r-- | brevisurl/urls.py | 8 | ||||
| -rw-r--r-- | brevisurl/utils.py | 18 | ||||
| -rw-r--r-- | brevisurl/views.py | 36 |
23 files changed, 498 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bab1113 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +*.pyo +*.swp +*.swo +.idea +pip-log.txt* +*.swn +/dist/ @@ -0,0 +1,26 @@ +The New BSD License + +Copyright (c) 2012, CodeScale s.r.o. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of CodeScale s.r.o. nor the names of its contributors + may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/README.rst diff --git a/brevisurl/__init__.py b/brevisurl/__init__.py new file mode 100644 index 0000000..66f2efa --- /dev/null +++ b/brevisurl/__init__.py @@ -0,0 +1,37 @@ +import brevisurl.settings +from brevisurl.utils import load_object + + +def get_connection(backend=None, fail_silently=False, **kwargs): + """Load a brevisurl backend and return an instance of it. + + If backend is None (default) settings.BREVISURL_BACKEND is used. + Both fail_silently and other keyword arguments are used in the + constructor of the backend. + + :param backend: path to brevisurl backend + :type backend: string + :param fail_silently: whether to fail silently when error intercepted or not + :type fail_silently: bool + :returns: instance os BaseBrevisUrlBackend + :rtype: brevisurl.backends.base.BaseBrevisUrlBackend + + """ + path = backend or brevisurl.settings.DEFAULT_BACKEND + klass = load_object(path) + return klass(fail_silently=fail_silently, **kwargs) + + +def shorten_url(original_url, fail_silently=False, connection=None): + """Shortcut util for shortening urls using default brevisurl backend if none supplied. + + :param original_url: url that will be shortened + :type original_url: string + :param backend: one of brevisurl backends + :type backend: brevisurl.backends.BaseBrevisUrlBackend + :returns: shortened url from original url + :rtype: brevisurl.models.ShortUrl + + """ + connection = connection or get_connection(fail_silently=fail_silently) + return connection.shorten_url(original_url)
\ No newline at end of file diff --git a/brevisurl/backends/__init__.py b/brevisurl/backends/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/brevisurl/backends/__init__.py diff --git a/brevisurl/backends/base.py b/brevisurl/backends/base.py new file mode 100644 index 0000000..3fc053a --- /dev/null +++ b/brevisurl/backends/base.py @@ -0,0 +1,41 @@ +from abc import ABCMeta + + +class BaseBrevisUrlBackend(object): + """Base class for brevisurl backend implementations. Subclasses must at least overwrite shorten_url().""" + + __metaclass__ = ABCMeta + + + def __init__(self, fail_silently=False, **kwargs): + self.fail_silently = fail_silently + self.class_path = '{0}.{1}'.format(self.__module__, self.__class__.__name__) + + def open(self): + """Open a network connection. + + This method can be overwritten by backend implementations to + open a network connection. + + It's up to the backend implementation to track the status of + a network connection if it's needed by the backend. + + The default implementation does nothing. + + """ + pass + + def close(self): + """Close a network connection.""" + pass + + def shorten_url(self, original_url): + """Shortens url into more compact form. + + :param original_url: url that will be shortened + :type original_url: string + :returns: shortened url from original url + :rtype: brevisurl.models.ShortUrl + + """ + raise NotImplementedError
\ No newline at end of file diff --git a/brevisurl/backends/local.py b/brevisurl/backends/local.py new file mode 100644 index 0000000..42d0263 --- /dev/null +++ b/brevisurl/backends/local.py @@ -0,0 +1,53 @@ +import string +import random +import logging + +from django.contrib.sites.models import Site + +from brevisurl.backends.base import BaseBrevisUrlBackend +from brevisurl.models import ShortUrl + + +log = logging.getLogger(__name__) + + +class BrevisUrlBackend(BaseBrevisUrlBackend): + + PROTOCOL = 'http' + + def shorten_url(self, original_url): + """ + :raises: ImproperlyConfigured, django.core.exceptions.ValidationError + """ + try: + short_url = ShortUrl.objects.get(backend=self.class_path, original_url=original_url) + log.info('Url "%s" already shortened to "%s"', original_url, short_url.shortened_url) + return short_url + except ShortUrl.DoesNotExist: + pass + + try: + current_site = Site.objects.get_current() + short_url = ShortUrl() + short_url.original_url = original_url + short_url.shortened_url = '{0}://{1}/{2}'.format(self.PROTOCOL, current_site.domain, + self.__generate_token()) + short_url.backend = self.class_path + short_url.save() + log.info('Url "%s" shortened to "%s"', original_url, short_url.shortened_url) + return short_url + except Exception: + if self.fail_silently: + return None + else: + log.exception('Unknown exception raised while shortening url "%s"', original_url) + raise + + def __generate_token(self, size=5): + chars = list(string.ascii_letters + string.digits) + random.shuffle(chars) + while True: + token = ''.join([random.choice(chars) for x in range(size)]) + if not ShortUrl.objects.filter(backend=self.class_path, shortened_url__endswith=token).count(): + break + return token
\ No newline at end of file diff --git a/brevisurl/models.py b/brevisurl/models.py new file mode 100644 index 0000000..a9264ab --- /dev/null +++ b/brevisurl/models.py @@ -0,0 +1,58 @@ +import hashlib +import logging +from django.core.exceptions import ValidationError + +from django.db import models +from django.core.validators import URLValidator + +from brevisurl import get_connection + + +log = logging.getLogger(__name__) + + +class ShortUrl(models.Model): + """Model that represents shortened url.""" + original_url = models.URLField(null=False, blank=False) + original_url_hash = models.CharField(max_length=64, null=False, blank=False) + shortened_url = models.URLField(max_length=200, null=False, blank=False, unique=True) + backend = models.CharField(max_length=200, null=False, blank=False) + created = models.DateTimeField(auto_now_add=True, db_index=True, null=False, blank=False) + + def __unicode__(self): + return self.shortened_url + + def get_connection(self, fail_silently=False): + if not hasattr(self, 'brevis_connection'): + if self.pk is not None: + self.brevis_connection = get_connection(backend=self.backend, fail_silently=fail_silently) + else: + self.brevis_connection = get_connection(fail_silently=fail_silently) + return self.brevis_connection + + def clean(self): + url_validator = URLValidator() + try: + url_validator(self.original_url) + except ValidationError: + log.exception('ShortUrl.original_url is not valid URL') + raise + try: + url_validator(self.shortened_url) + except ValidationError: + log.exception('ShortUrl.shortened_url is not valid URL') + raise + return super(ShortUrl, self).clean() + + def save(self, force_insert=False, force_update=False, using=None): + if self.pk is None: + self.original_url_hash = hashlib.sha256(self.original_url).hexdigest() + self.full_clean() + return super(ShortUrl, self).save(force_insert, force_update, using) + + class Meta: + unique_together = (('original_url_hash', 'backend'),) + verbose_name = 'Short url' + verbose_name_plural = 'Short urls' + ordering = ['-created'] + get_latest_by = 'created'
\ No newline at end of file diff --git a/brevisurl/settings.py b/brevisurl/settings.py new file mode 100644 index 0000000..590f8b1 --- /dev/null +++ b/brevisurl/settings.py @@ -0,0 +1,3 @@ +from django.conf import settings + +DEFAULT_BACKEND = getattr(settings, 'BREVISURL_BACKEND', 'brevisurl.backends.local.BrevisUrlBackend')
\ No newline at end of file diff --git a/brevisurl/templatetags/__init__.py b/brevisurl/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/brevisurl/templatetags/__init__.py diff --git a/brevisurl/templatetags/brevisurltags.py b/brevisurl/templatetags/brevisurltags.py new file mode 100644 index 0000000..2d883f0 --- /dev/null +++ b/brevisurl/templatetags/brevisurltags.py @@ -0,0 +1,11 @@ +from django import template + +from brevisurl import shorten_url as shorten_url_util, get_connection + +register = template.Library() + + +@register.filter +def shorten_url(original_url): + short_url = shorten_url_util(original_url, connection=get_connection(fail_silently=True)) + return short_url.shortened_url if short_url is not None else original_url
\ No newline at end of file diff --git a/brevisurl/tests/__init__.py b/brevisurl/tests/__init__.py new file mode 100644 index 0000000..729e40a --- /dev/null +++ b/brevisurl/tests/__init__.py @@ -0,0 +1,6 @@ +from brevisurl.tests.test_brevisurl import * +from brevisurl.tests.test_utils import * +from brevisurl.tests.test_views import * +from brevisurl.tests.test_models import * +from brevisurl.tests.backends.test_local import * +from brevisurl.tests.templatetags.test_brevisurltags import *
\ No newline at end of file diff --git a/brevisurl/tests/backends/__init__.py b/brevisurl/tests/backends/__init__.py new file mode 100644 index 0000000..7a8337f --- /dev/null +++ b/brevisurl/tests/backends/__init__.py @@ -0,0 +1 @@ +__author__ = 'char0n' diff --git a/brevisurl/tests/backends/test_local.py b/brevisurl/tests/backends/test_local.py new file mode 100644 index 0000000..8939314 --- /dev/null +++ b/brevisurl/tests/backends/test_local.py @@ -0,0 +1,55 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase +from django.core.validators import URLValidator + +from brevisurl import get_connection +from brevisurl.models import ShortUrl + + +class TestLocalBrevisUrlBackend(TestCase): + + def setUp(self): + self.connection = get_connection('brevisurl.backends.local.BrevisUrlBackend') + + def test_shorten_url(self): + self.assertEqual(ShortUrl.objects.all().count(), 0) + original_url = 'http://www.codescale.net/' + short_url = self.connection.shorten_url(original_url) + self.assertEqual(ShortUrl.objects.all().count(), 1) + self.assertEqual(short_url.original_url, original_url) + self.assertRegexpMatches(short_url.shortened_url, URLValidator.regex) + + def test_shorten_url_reuse_old(self): + original_url = 'http://www.codescale.net/' + self.assertEqual(ShortUrl.objects.all().count(), 0) + short_url = self.connection.shorten_url(original_url) + self.assertEqual(ShortUrl.objects.all().count(), 1) + self.assertEqual(short_url.original_url, original_url) + self.assertRegexpMatches(short_url.shortened_url, URLValidator.regex) + short_url = self.connection.shorten_url(original_url) + self.assertEqual(ShortUrl.objects.all().count(), 1) + self.assertEqual(short_url.original_url, original_url) + self.assertRegexpMatches(short_url.shortened_url, URLValidator.regex) + + def test_shorten_url_create_new(self): + original_url = 'http://www.codescale.net/' + self.assertEqual(ShortUrl.objects.all().count(), 0) + short_url = self.connection.shorten_url(original_url) + self.assertEqual(ShortUrl.objects.all().count(), 1) + self.assertEqual(short_url.original_url, original_url) + self.assertRegexpMatches(short_url.shortened_url, URLValidator.regex) + original_url = 'http://www.codescale.net/en/company/' + short_url = self.connection.shorten_url(original_url) + self.assertEqual(ShortUrl.objects.all().count(), 2) + self.assertEqual(short_url.original_url, original_url) + self.assertRegexpMatches(short_url.shortened_url, URLValidator.regex) + + def test_shorten_url_invalid_original_url(self): + with self.assertRaises(ValidationError): + self.connection.shorten_url('www.codescale.') + self.assertEqual(ShortUrl.objects.all().count(), 0) + + def test_shorten_url_invalid_original_url_fail_silently(self): + self.connection.fail_silently = True + shorl_url = self.connection.shorten_url('www.codescale.') + self.assertIsNone(shorl_url)
\ No newline at end of file diff --git a/brevisurl/tests/templatetags/__init__.py b/brevisurl/tests/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/brevisurl/tests/templatetags/__init__.py diff --git a/brevisurl/tests/templatetags/test_brevisurltags.py b/brevisurl/tests/templatetags/test_brevisurltags.py new file mode 100644 index 0000000..de1ed00 --- /dev/null +++ b/brevisurl/tests/templatetags/test_brevisurltags.py @@ -0,0 +1,27 @@ +from django.core.validators import URLValidator +from django.test import TestCase +from django.template import Template, Context + +from brevisurl.models import ShortUrl + + +class TestShortenUrlTag(TestCase): + + def test_shorten_url_tag(self): + original_url = 'http://www.codescale.net/' + url = Template(""" + {% load brevisurltags %} + {{ url|shorten_url }} + """).render(Context({'url': original_url})).strip() + self.assertEqual(ShortUrl.objects.all().count(), 1) + self.assertEqual(ShortUrl.objects.all()[0].original_url, original_url) + self.assertRegexpMatches(url, URLValidator.regex) + + def test_shorten_url_tag_invalid_url(self): + original_url = 'www.codescale.' + url = Template(""" + {% load brevisurltags %} + {{ url|shorten_url }} + """).render(Context({'url': original_url})).strip() + self.assertEqual(ShortUrl.objects.all().count(), 0) + self.assertEqual(url, original_url)
\ No newline at end of file diff --git a/brevisurl/tests/test_brevisurl.py b/brevisurl/tests/test_brevisurl.py new file mode 100644 index 0000000..8780b05 --- /dev/null +++ b/brevisurl/tests/test_brevisurl.py @@ -0,0 +1,22 @@ +from django.utils import unittest + +from brevisurl import get_connection, shorten_url +from brevisurl.backends.local import BrevisUrlBackend +from brevisurl.backends.base import BaseBrevisUrlBackend + + +class TestGetConnection(unittest.TestCase): + + def test_get_default_connection(self): + connection = get_connection() + self.assertIsInstance(connection, BrevisUrlBackend) + + def test_get_custom_connection(self): + base_connection = get_connection(backend='brevisurl.backends.base.BaseBrevisUrlBackend') + local_connection = get_connection(backend='brevisurl.backends.local.BrevisUrlBackend') + self.assertIsInstance(base_connection, BaseBrevisUrlBackend) + self.assertIsInstance(local_connection, BrevisUrlBackend) + + def test_get_connection_non_existing_backend(self): + with self.assertRaises(AttributeError): + get_connection(backend='brevisurl.backends.local.NonExistingBackend')
\ No newline at end of file diff --git a/brevisurl/tests/test_models.py b/brevisurl/tests/test_models.py new file mode 100644 index 0000000..6942124 --- /dev/null +++ b/brevisurl/tests/test_models.py @@ -0,0 +1,38 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase +from django.contrib.sites.models import Site + +from brevisurl.models import ShortUrl +from brevisurl import get_connection + + +class TestModels(TestCase): + + def test_model_save(self): + site = Site.objects.get_current() + connection = get_connection('brevisurl.backends.local.BrevisUrlBackend') + short_url = ShortUrl() + short_url.original_url = 'http://www.codescale.net/' + short_url.shortened_url = '{0}://{1}/12345'.format(connection.PROTOCOL, site.domain) + short_url.backend = connection.class_path + short_url.save() + self.assertIsNotNone(short_url.pk) + + def test_model_save_invalid_original_url(self): + with self.assertRaises(ValidationError): + self.site = Site.objects.get_current() + self.connection = get_connection('brevisurl.backends.local.BrevisUrlBackend') + self.short_url = ShortUrl() + self.short_url.original_url = 'www.codescale.' + self.short_url.shortened_url = '{0}://{1}/12345'.format(self.connection.PROTOCOL, self.site.domain) + self.short_url.backend = self.connection.class_path + self.short_url.save() + + def test_model_save_invalid_shortened_url(self): + with self.assertRaises(ValidationError): + connection = get_connection('brevisurl.backends.local.BrevisUrlBackend') + short_url = ShortUrl() + short_url.original_url = 'http://www.codescale.net/' + short_url.shortened_url = 'www.codescale.' + short_url.backend = connection.class_path + short_url.save()
\ No newline at end of file diff --git a/brevisurl/tests/test_utils.py b/brevisurl/tests/test_utils.py new file mode 100644 index 0000000..6d31e77 --- /dev/null +++ b/brevisurl/tests/test_utils.py @@ -0,0 +1,22 @@ +from django.utils import unittest + +from brevisurl.utils import load_object +from brevisurl.backends.local import BrevisUrlBackend + +class TestLoadObject(unittest.TestCase): + + def test_load_valid_object(self): + klass = load_object('brevisurl.backends.local.BrevisUrlBackend') + self.assertEqual(klass, BrevisUrlBackend) + + def test_load_import_error(self): + with self.assertRaises(ImportError): + load_object('brevisurl.local.BrevisUrlBackend') + + def test_load_attribute_error(self): + with self.assertRaises(AttributeError): + load_object('brevisurl.backends.local.NonExistingBackend') + + def test_load_value_error(self): + with self.assertRaises(ValueError): + load_object('brevisurl')
\ No newline at end of file diff --git a/brevisurl/tests/test_views.py b/brevisurl/tests/test_views.py new file mode 100644 index 0000000..ca20c43 --- /dev/null +++ b/brevisurl/tests/test_views.py @@ -0,0 +1,28 @@ +from django.test import TestCase +from django.test.client import Client +from django.core.urlresolvers import reverse +from django.contrib.sites.models import Site + +from brevisurl import get_connection +from brevisurl.models import ShortUrl + + +class TestBrevisUrlRedirectView(TestCase): + + def setUp(self): + self.site = Site.objects.get_current() + self.connection = get_connection('brevisurl.backends.local.BrevisUrlBackend') + self.short_url = ShortUrl() + self.short_url.original_url = 'http://www.codescale.net/' + self.short_url.shortened_url = '{0}://{1}/12345'.format(self.connection.PROTOCOL, self.site.domain) + self.short_url.backend = self.connection.class_path + self.short_url.save() + self.client = Client() + + def test_redirect(self): + response = self.client.get(reverse('brevisurl_redirect', kwargs={'token': 12345})) + self.assertEqual(response.status_code, 301) + + def test_redirect_non_existing_token(self): + response = self.client.get(reverse('brevisurl_redirect', kwargs={'token': 54321})) + self.assertEqual(response.status_code, 404)
\ No newline at end of file diff --git a/brevisurl/urls.py b/brevisurl/urls.py new file mode 100644 index 0000000..e2ca7e9 --- /dev/null +++ b/brevisurl/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import patterns, url + +from brevisurl import views + + +urlpatterns = patterns('brevisurl.views', + url(r'^(?P<token>[a-zA-Z0-9]{5})$', views.BrevisUrlRedirectView.as_view(), name='brevisurl_redirect'), +)
\ No newline at end of file diff --git a/brevisurl/utils.py b/brevisurl/utils.py new file mode 100644 index 0000000..e0c3d52 --- /dev/null +++ b/brevisurl/utils.py @@ -0,0 +1,18 @@ +from django.utils import importlib + + +def load_object(import_path): + """Util for importing objects from import path. + + :param import_path: import path of object to be imported e.g. module.submodule.Class + :type import_path: string + :returns: imported object + :rtype: object + :raises: ValueError, ImportError, AttributeError + + """ + if not (isinstance(import_path, basestring) and '.' in import_path): + raise ValueError('There must be at least one dot in import path: "%s"', import_path) + module_name, object_name = import_path.rsplit('.', 1) + module = importlib.import_module(module_name) + return getattr(module, object_name)
\ No newline at end of file diff --git a/brevisurl/views.py b/brevisurl/views.py new file mode 100644 index 0000000..21cd1a5 --- /dev/null +++ b/brevisurl/views.py @@ -0,0 +1,36 @@ +import logging + +from django.http import Http404 +from django.contrib.sites.models import Site +from django.views.generic.base import RedirectView +from django.core.exceptions import ImproperlyConfigured + +from brevisurl import get_connection +from brevisurl.models import ShortUrl + + +log = logging.getLogger(__name__) + + +class BrevisUrlRedirectView(RedirectView): + + def get_redirect_url(self, **kwargs): + try: + token = kwargs.pop('token') + except KeyError: + log.exception('Token not found in keyword arguments') + raise Http404 + connection = get_connection('brevisurl.backends.local.BrevisUrlBackend') + protocol = connection.PROTOCOL + try: + site = Site.objects.get_current() + except ImproperlyConfigured: + log.exception('No site object configured for this django project') + raise Http404 + try: + short_url = '{0}://{1}/{2}'.format(protocol, site.domain, token) + short_url_obj = ShortUrl.objects.get(backend=connection.class_path, shortened_url=short_url) + except ShortUrl.DoesNotExist: + log.exception('No shortened url found for backend: "%s" and token: "%s"', connection.class_path, token) + raise Http404 + return short_url_obj.original_url
\ No newline at end of file |
