Commits

jgsogo committed 2ed43ba Draft

allow to stablish min characters on hash. Updated tests (settings failures)

  • Participants
  • Parent commits 3f16611

Comments (0)

Files changed (5)

File shortener/models.py

 from django.core.validators import URLValidator
 from django.core.exceptions import ValidationError
 
-from shortener.settings import LINK_UNIQUENESS, SITE_BASE_URL, HASH_STRATEGY, LINK_MIXIN, MAX_HASH_LENGTH
+from shortener.settings import LINK_UNIQUENESS, SITE_BASE_URL, HASH_STRATEGY, LINK_MIXIN, MAX_HASH_LENGTH, MIN_HASH_LENGTH
 from shortener.utils.baseconv import base62
 from shortener.utils import get_basemodel_mixin
 
             return cls.create(url)
 
     @classmethod
-    def create(cls, url, commit=True):
+    def create(cls, url):
         log.debug("Link::create(url='%s')" % url)
         if not cls.url_is_valid(url):
             raise ValueError('Invalid URL')
             raise ValueError('Link already exists')
 
         instance = cls()
-        instance._hash = cls.generate_unique_hash(instance)
+        instance._hash = cls.generate_unique_hash(instance, MIN_HASH_LENGTH, MAX_HASH_LENGTH)
         instance.url = url
 
-        if commit:
-            instance.save()
+        instance.save()
         return instance
 
     @classmethod
-    def generate_unique_hash(cls, instance):
+    def generate_unique_hash(cls, instance, min_hash_length, max_hash_length):
         if HASH_STRATEGY:
-            hash = HASH_STRATEGY(instance, MAX_HASH_LENGTH)
+            hash = HASH_STRATEGY(instance, min_hash_length, max_hash_length)
             if cls.hash_exists(hash):
                 raise ValueError("Generated hash already exists")
             return hash
         else:
             if not instance.id:
                 instance.save() # To get an id.
-            return cls.baseconverter.from_decimal(instance.id)
+            initial_count = pow(len(cls.baseconverter.digits), min_hash_length-1)
+            return cls.baseconverter.from_decimal(initial_count + instance.id)
 
     @classmethod
     def hash_exists(cls, hash_):
         except ValidationError:
             return False
 
+    @classmethod
+    def max_shortened_available(cls):
+        chars = len(cls.baseconverter.digits)
+        return pow(chars, MAX_HASH_LENGTH) - pow(chars, MIN_HASH_LENGTH)
 
 if LINK_MIXIN:
     Mixin = get_basemodel_mixin(LINK_MIXIN)

File shortener/settings.py

 
 HASH_STRATEGY = getattr(settings, 'SHORTENER_HASH_STRATEGY', None)
 MAX_HASH_LENGTH = getattr(settings, 'SHORTENER_MAX_HASH_LENGTH', 8)
+MIN_HASH_LENGTH = getattr(settings, 'SHORTENER_MIN_HASH_LENGTH', 4)
 
 SITE_BASE_URL = getattr(settings, 'SHORTENER_SITE_BASE_URL')
 

File shortener/tests/baseconv.py

     """
 
     def _baseconv_test(self, str_code, max_chars=8):
+        zero_alike = str_code[0]
         log.debug("Test baseconv with str_code '%s' (%s)" % (str_code, len(str_code)))
         bin = BaseConverter(str_code)
         base = len(str_code)
             log.debug("Test reverse conversion for number '%s'" % number)
             self.assertEqual(number, bin.to_decimal(bin.from_decimal(number)))
             char_str = ''.join(random.choice(str_code) for x in range(max_chars))
-            char_str = re.sub("^0+", "", char_str) # Remove leading zeros
+            char_str = re.sub("^%s+"%zero_alike, "", char_str) # Remove leading zeros
             log.debug("Test reverse conversion for char string '%s'" % char_str)
             self.assertEqual(char_str, bin.from_decimal(bin.to_decimal(char_str)))
 

File shortener/tests/hash.py

 log = logging.getLogger(__name__)
 
 @override_settings(SHORTENER_LINK_UNIQUENESS=True)
+@override_settings(SHORTENER_MIN_HASH_LENGTH=1)
 @override_settings(SHORTENER_MAX_HASH_LENGTH=2)
 class HashTest(TestCase):
 
+    class LinkProxyModel(object):
+        def __init__(self, id):
+            self.id = id
+        def save(self):
+            pass
+
     def test_length(self):
-        max_possibilities = int(pow(len(Link.baseconverter.digits), settings.SHORTENER_MAX_HASH_LENGTH))
+        max_possibilities = int(pow(len(Link.baseconverter.digits), settings.SHORTENER_MAX_HASH_LENGTH)) # TODO: how to apply modified settings to test? int(Link.max_shortened_available())
         log.debug("Testing uniqueness for %s possibilities" % max_possibilities)
+        instance = HashTest.LinkProxyModel(0)
         for i in xrange(max_possibilities-1):
-            obj = Link.create('http://this.isvalid.url')
-        self.assertEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(obj._hash))
+            hash = Link.generate_unique_hash(instance, settings.SHORTENER_MIN_HASH_LENGTH, settings.SHORTENER_MAX_HASH_LENGTH)
+            instance.id += 1
+        self.assertEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(hash))
         # Next one will have one more char (model raises exception)
-        obj = Link.create('http://this.isvalid.url')
-        self.assertNotEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(obj._hash))
+        hash = Link.generate_unique_hash(instance, settings.SHORTENER_MIN_HASH_LENGTH, settings.SHORTENER_MAX_HASH_LENGTH)
+        self.assertNotEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(hash))
 
     @override_settings(SHORTENER_HASH_STRATEGY=generate_unique_random_hash)
     def test_random(self):
-        max_possibilities = int(pow(len(Link.baseconverter.digits), settings.SHORTENER_MAX_HASH_LENGTH))
+        max_possibilities = int(pow(len(Link.baseconverter.digits), settings.SHORTENER_MAX_HASH_LENGTH)) # TODO: how to apply modified settings to test? int(Link.max_shortened_available())
         log.debug("Testing uniqueness for %s possibilities" % max_possibilities)
+        instance = HashTest.LinkProxyModel(0)
         for i in xrange(max_possibilities-1):
-            obj = Link.create('http://this.isvalid.url')
-        self.assertEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(obj._hash))
+            hash = Link.generate_unique_hash(instance, settings.SHORTENER_MIN_HASH_LENGTH, settings.SHORTENER_MAX_HASH_LENGTH)
+        self.assertEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(hash))
         # Next one will have one more char.
-        obj = Link.create('http://this.isvalid.url')
-        self.assertNotEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(obj._hash))
+        hash = Link.generate_unique_hash(instance, settings.SHORTENER_MIN_HASH_LENGTH, settings.SHORTENER_MAX_HASH_LENGTH)
+        self.assertNotEqual(settings.SHORTENER_MAX_HASH_LENGTH, len(hash))

File shortener/utils/hash_random.py

 
 log = logging.getLogger(__name__)
 
-def generate_random_hash(max_length):
+def generate_random_hash(min_length, max_length):
     return base64.urlsafe_b64encode(os.urandom(max_length)).strip('=')
 
-def generate_unique_random_hash(instance, max_length, i=0):
-    hash = generate_random_hash(max_length)[:max_length]
+def generate_unique_random_hash(instance, min_length, max_length, i=0):
+    hash = generate_random_hash(min_length, max_length)[:max_length]
     if Link.hash_exists(hash):
         if (i>=100):
             log.warn('Hashes are clashing, consider increasing max_hash_length.')
-        return generate_unique_random_hash(instance, max_length, i=i+1)
+        return generate_unique_random_hash(instance, min_length, max_length, i=i+1)
     else:
         return hash