Repository: getpelican/pelican

Issue:
RFE: Add slugs for tags for backward compatibility with existing URLs
Hi guys,
I've migrated to Pelican from Octropress and managed to keep all articles URLs the same but had troubles with some tags. If your tag contains a dot (e.g. fedora.planet) then its sluge removes the dot (becomes fedoraplanet) and I don't see any way to keep my existing tag URL. 



Discussion:

Resolving patch:
diff --git a/pelican/contents.py b/pelican/contents.py
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -172,6 +172,7 @@ def url_format(self):
             'lang': getattr(self, 'lang', 'en'),
             'date': getattr(self, 'date', SafeDatetime.now()),
             'author': self.author.slug if hasattr(self, 'author') else '',
+            'tag': self.tag.slug if hasattr(self, 'tag') else '',
             'category': self.category.slug if hasattr(self, 'category') else ''
         })
         return metadata
diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py
--- a/pelican/urlwrappers.py
+++ b/pelican/urlwrappers.py
@@ -112,13 +112,33 @@ def _from_settings(self, key, get_page_name=False):
 
 
 class Category(URLWrapper):
-    pass
+    @property
+    def slug(self):
+        if self._slug is None:
+            substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())
+            substitutions += tuple(self.settings.get('CATEGORY_SUBSTITUTIONS',
+                                                     ()))
+            self._slug = slugify(self.name, substitutions)
+        return self._slug
 
 
 class Tag(URLWrapper):
     def __init__(self, name, *args, **kwargs):
         super(Tag, self).__init__(name.strip(), *args, **kwargs)
 
+    @property
+    def slug(self):
+        if self._slug is None:
+            substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ())
+            substitutions += tuple(self.settings.get('TAG_SUBSTITUTIONS', ()))
+            self._slug = slugify(self.name, substitutions)
+        return self._slug
+
 
 class Author(URLWrapper):
-    pass
+    @property
+    def slug(self):
+        if self._slug is None:
+            self._slug = slugify(self.name,
+                                 self.settings.get('AUTHOR_SUBSTITUTIONS', ()))
+        return self._slug
diff --git a/pelican/utils.py b/pelican/utils.py
--- a/pelican/utils.py
+++ b/pelican/utils.py
@@ -270,10 +270,34 @@ def slugify(value, substitutions=()):
         value = value.decode('ascii')
     # still unicode
     value = unicodedata.normalize('NFKD', value).lower()
-    for src, dst in substitutions:
+
+    # backward compatible covert from 2-tuples to 3-tuples
+    new_subs = []
+    for tpl in substitutions:
+        try:
+            src, dst, skip = tpl
+        except ValueError:
+            src, dst = tpl
+            skip = False
+        new_subs.append((src, dst, skip))
+    substitutions = tuple(new_subs)
+
+    # by default will replace non-alphanum characters
+    replace = True
+    for src, dst, skip in substitutions:
+        orig_value = value
         value = value.replace(src.lower(), dst.lower())
-    value = re.sub('[^\w\s-]', '', value).strip()
-    value = re.sub('[-\s]+', '-', value)
+        # if replacement was made then skip non-alphanum
+        # replacement if instructed to do so
+        if value != orig_value:
+            replace = replace and not skip
+
+    if replace:
+        value = re.sub('[^\w\s-]', '', value).strip()
+        value = re.sub('[-\s]+', '-', value)
+    else:
+        value = value.strip()
+
     # we want only ASCII chars
     value = value.encode('ascii', 'ignore')
     # but Pelican should generally use only unicode


Test patch:
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
--- a/pelican/tests/test_contents.py
+++ b/pelican/tests/test_contents.py
@@ -11,7 +11,7 @@
 
 import six
 
-from pelican.contents import Article, Author, Category, Page, Static
+from pelican.contents import Article, Author, Category, Page, Static, Tag
 from pelican.settings import DEFAULT_CONFIG
 from pelican.signals import content_object_init
 from pelican.tests.support import LoggedTestCase, get_settings, unittest
@@ -457,6 +457,46 @@ def test_slugify_category_author(self):
         self.assertEqual(
             article.save_as, 'obrien/csharp-stuff/fnord/index.html')
 
+    def test_slugify_with_author_substitutions(self):
+        settings = get_settings()
+        settings['AUTHOR_SUBSTITUTIONS'] = [
+                                    ('Alexander Todorov', 'atodorov', False),
+                                    ('Krasimir Tsonev', 'krasimir', False),
+        ]
+        settings['ARTICLE_URL'] = 'blog/{author}/{slug}/'
+        settings['ARTICLE_SAVE_AS'] = 'blog/{author}/{slug}/index.html'
+        article_kwargs = self._copy_page_kwargs()
+        article_kwargs['metadata']['author'] = Author('Alexander Todorov',
+                                                      settings)
+        article_kwargs['metadata']['title'] = 'fnord'
+        article_kwargs['settings'] = settings
+        article = Article(**article_kwargs)
+        self.assertEqual(article.url, 'blog/atodorov/fnord/')
+        self.assertEqual(article.save_as, 'blog/atodorov/fnord/index.html')
+
+    def test_slugify_category_with_dots(self):
+        settings = get_settings()
+        settings['CATEGORY_SUBSTITUTIONS'] = [('Fedora QA', 'fedora.qa', True)]
+        settings['ARTICLE_URL'] = '{category}/{slug}/'
+        article_kwargs = self._copy_page_kwargs()
+        article_kwargs['metadata']['category'] = Category('Fedora QA',
+                                                          settings)
+        article_kwargs['metadata']['title'] = 'This Week in Fedora QA'
+        article_kwargs['settings'] = settings
+        article = Article(**article_kwargs)
+        self.assertEqual(article.url, 'fedora.qa/this-week-in-fedora-qa/')
+
+    def test_slugify_tags_with_dots(self):
+        settings = get_settings()
+        settings['TAG_SUBSTITUTIONS'] = [('Fedora QA', 'fedora.qa', True)]
+        settings['ARTICLE_URL'] = '{tag}/{slug}/'
+        article_kwargs = self._copy_page_kwargs()
+        article_kwargs['metadata']['tag'] = Tag('Fedora QA', settings)
+        article_kwargs['metadata']['title'] = 'This Week in Fedora QA'
+        article_kwargs['settings'] = settings
+        article = Article(**article_kwargs)
+        self.assertEqual(article.url, 'fedora.qa/this-week-in-fedora-qa/')
+
 
 class TestStatic(LoggedTestCase):
 
diff --git a/pelican/tests/test_urlwrappers.py b/pelican/tests/test_urlwrappers.py
--- a/pelican/tests/test_urlwrappers.py
+++ b/pelican/tests/test_urlwrappers.py
@@ -56,3 +56,34 @@ def test_equality(self):
 
         cat_ascii = Category('指導書', settings={})
         self.assertEqual(cat_ascii, u'zhi-dao-shu')
+
+    def test_slugify_with_substitutions_and_dots(self):
+        tag = Tag('Tag Dot',
+                  settings={
+                        'TAG_SUBSTITUTIONS': [('Tag Dot', 'tag.dot', True)]
+                    })
+        cat = Category('Category Dot',
+                       settings={
+                        'CATEGORY_SUBSTITUTIONS': (('Category Dot',
+                                                    'cat.dot',
+                                                    True),)
+                        })
+
+        self.assertEqual(tag.slug, 'tag.dot')
+        self.assertEqual(cat.slug, 'cat.dot')
+
+    def test_author_slug_substitutions(self):
+        settings = {
+            'AUTHOR_SUBSTITUTIONS': [
+                                    ('Alexander Todorov', 'atodorov', False),
+                                    ('Krasimir Tsonev', 'krasimir', False),
+            ]
+        }
+
+        author1 = Author('Mr. Senko', settings=settings)
+        author2 = Author('Alexander Todorov', settings=settings)
+        author3 = Author('Krasimir Tsonev', settings=settings)
+
+        self.assertEqual(author1.slug, 'mr-senko')
+        self.assertEqual(author2.slug, 'atodorov')
+        self.assertEqual(author3.slug, 'krasimir')
diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py
--- a/pelican/tests/test_utils.py
+++ b/pelican/tests/test_utils.py
@@ -131,6 +131,18 @@ def test_slugify_substitute(self):
         for value, expected in samples:
             self.assertEqual(utils.slugify(value, subs), expected)
 
+    def test_slugify_substitute_and_keeping_non_alphanum(self):
+
+        samples = (('Fedora QA', 'fedora.qa'),
+                   ('C++ is used by Fedora QA', 'cpp is used by fedora.qa'),
+                   ('C++ is based on C', 'cpp-is-based-on-c'),
+                   ('C+++ test C+ test', 'cpp-test-c-test'),)
+
+        subs = (('Fedora QA', 'fedora.qa', True),
+                ('c++', 'cpp'),)
+        for value, expected in samples:
+            self.assertEqual(utils.slugify(value, subs), expected)
+
     def test_get_relative_path(self):
 
         samples = ((os.path.join('test', 'test.html'), os.pardir),