Repository: getpelican/pelican

Issue:
Conflicts rendering Category pages when category is not defined in consistent case
I was testing a jinja macro that dealt with creating links for categories.

I noted that if you define a category in one article as `Category: Something` and in another article as `Category: something` that these are treated as separate categories, however, when your category page is rendered, there is only the lowecase url, e.g. `category/something.html`. This will only associate with the articles with meta data defined as `Category: something` and not anywhere where it is defined with uppercase since there is no `category/Something.html`.

I am not sure if making this case insensitive would break code. Certainly, it would be unclear when printing the category name which case to use. From an intelligent template process, you would set you case using CSS style attribute to be sure it was the way you want, and it could always render categories in lower case.

Otherwise, it might just be sufficient to put this into the documentation. I always tend to capitalize by categories, but some people might not notice and wonder why some articles are missing. I have not yet tested this, but I would imagine the same issue exists for tags.



Discussion:
To fix this the `categories` `dict` in `generators.py` needs to be keyed by the category slug rather than the category name. This does present another problem though of which category name to show on the category page (Something vs. something).

I would believe the default should be that all names are lowecase, you can then apply a uppercase function on the output or use CSS styling if you want to ensure proper case display.

On Sun, Mar 24, 2013 at 08:17:25PM -0700, Eric wrote:

> I would believe the default should be that all names are lowecase,
> you can then apply a uppercase function on the output or use CSS
> styling if you want to ensure proper case display.

It is conceivable that you'd want some of your categories in Title
Case, and some in UPPERCASE (e.g. acronyms).  That would be a hard
distinction to make in CSS.  To me, it seems like a better idea to
preserve case keying and display.  If you have the same category name
with two capitalizations, you get two different categories (with two
category feeds, etc.).  If you don't like that, pick one
capitalization and standardize your metadata ;).

@wking: With case-insensitive filesystems, such as HFS+ on Mac OS X, you won't get two different category feeds if you have two categories that only differ in terms of case.

That said, I tend to agree with what I presume to be your conclusion: it's easier to standardize your metadata than to solve this thorny problem at a technical level.

That said, folks are welcome to pursue something akin to what @rupert suggested, or submit a pull request to clarify the current behavior in the docs.

Perhaps a warning when two categories / tags have the same URL?


Resolving patch:
diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py
--- a/pelican/urlwrappers.py
+++ b/pelican/urlwrappers.py
@@ -13,21 +13,35 @@
 @functools.total_ordering
 class URLWrapper(object):
     def __init__(self, name, settings):
+        # next 2 lines are redundant with the setter of the name property
+        # but are here for clarity
+        self._name = name
+        self.slug = slugify(name)
         self.name = name
-        self.slug = slugify(self.name)
         self.settings = settings
 
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+        self.slug = slugify(name)
+
     def as_dict(self):
-        return self.__dict__
+        d = self.__dict__
+        d['name'] = self.name
+        return d
 
     def __hash__(self):
-        return hash(self.name)
+        return hash(self.slug)
 
     def _key(self):
-        return self.name
+        return self.slug
 
     def _normalize_key(self, key):
-        return six.text_type(key)
+        return six.text_type(slugify(key))
 
     def __eq__(self, other):
         return self._key() == self._normalize_key(other)


Test patch:
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -89,17 +89,29 @@ def test_generate_context(self):
             ['This is an article without category !', 'published',
              'TestCategory', 'article'],
             ['This is a super article !', 'published', 'yeah', 'article'],
-            ['マックOS X 10.8でパイソンとVirtualenvをインストールと設定', 'published', '指導書', 'article']
+            ['マックOS X 10.8でパイソンとVirtualenvをインストールと設定',
+             'published', '指導書', 'article']
         ]
         self.assertEqual(sorted(articles_expected), sorted(articles))
 
     def test_generate_categories(self):
 
         generator = self.get_populated_generator()
+        # test for name
+        # categories are grouped by slug; if two categories have the same slug
+        # but different names they will be grouped together, the first one in
+        # terms of process order will define the name for that category
         categories = [cat.name for cat, _ in generator.categories]
-        categories_expected = ['Default', 'TestCategory', 'Yeah', 'test',
-                               'yeah', '指導書']
-        self.assertEqual(categories, categories_expected)
+        categories_alternatives = (
+            sorted(['Default', 'TestCategory', 'Yeah', 'test', '指導書']),
+            sorted(['Default', 'TestCategory', 'yeah', 'test', '指導書']),
+        )
+        self.assertTrue(sorted(categories) in categories_alternatives)
+        # test for slug
+        categories = [cat.slug for cat, _ in generator.categories]
+        categories_expected = ['default', 'testcategory', 'yeah', 'test',
+                               'zhi-dao-shu']
+        self.assertEqual(sorted(categories), sorted(categories_expected))
 
     def test_do_not_use_folder_as_category(self):
 
@@ -113,9 +125,20 @@ def test_do_not_use_folder_as_category(self):
                             CUR_DIR, _DEFAULT_CONFIG['THEME'], None,
                             _DEFAULT_CONFIG['MARKUP'])
         generator.generate_context()
-
+        # test for name
+        # categories are grouped by slug; if two categories have the same slug
+        # but different names they will be grouped together, the first one in
+        # terms of process order will define the name for that category
         categories = [cat.name for cat, _ in generator.categories]
-        self.assertEqual(categories, ['Default', 'Yeah', 'test', 'yeah', '指導書'])
+        categories_alternatives = (
+            sorted(['Default', 'Yeah', 'test', '指導書']),
+            sorted(['Default', 'yeah', 'test', '指導書']),
+        )
+        self.assertTrue(sorted(categories) in categories_alternatives)
+        # test for slug
+        categories = [cat.slug for cat, _ in generator.categories]
+        categories_expected = ['default', 'yeah', 'test', 'zhi-dao-shu']
+        self.assertEqual(sorted(categories), sorted(categories_expected))
 
     def test_direct_templates_save_as_default(self):