From 38134a38d3971950f1d42facb9d1bfa53281c630 Mon Sep 17 00:00:00 2001 From: Eklavya Sharma Date: Sun, 11 Oct 2026 10:40:45 -0500 Subject: [PATCH] fix: Allow drafts without a category (#3600) Co-authored-by: Justin Mayer --- pelican/generators.py | 2 +- pelican/tests/test_generators.py | 68 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/pelican/generators.py b/pelican/generators.py index 515257c4..97c18db5 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -667,7 +667,7 @@ class ArticlesGenerator(CachingGenerator): self.get_template(draft.template), self.context, article=draft, - category=draft.category, + category=getattr(draft, "category", None), override_output=hasattr(draft, "override_save_as"), blog=True, all_articles=self.articles, diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index f566bc9c..a07bef7b 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -378,6 +378,74 @@ class TestArticlesGenerator(unittest.TestCase): ] self.assertEqual(sorted(draft_articles_expected), sorted(self.drafts)) + def test_generate_drafts_without_category(self): + """Drafts without a category must not break draft page generation.""" + content_dir = mkdtemp(prefix="pelican_test_drafts.") + try: + # Test one draft without any category metadata, one with an explicit + # category, and neither in a category subfolder. + with open(os.path.join(content_dir, "draft_no_category.md"), "w") as f: + f.write( + "Title: Draft without category\n" + "Date: 2024-01-01\n" + "Status: draft\n" + "\n" + "Draft content without a category.\n" + ) + with open(os.path.join(content_dir, "draft_with_category.md"), "w") as f: + f.write( + "Title: Draft with category\n" + "Date: 2024-01-02\n" + "Status: draft\n" + "Category: Test Category\n" + "\n" + "Draft content with a category.\n" + ) + + settings = get_settings(PATH=content_dir) + settings["CACHE_PATH"] = self.temp_cache + settings["USE_FOLDER_AS_CATEGORY"] = False + settings["CATEGORY_SAVE_AS"] = None # No default category fallback + context = get_context(settings) + + generator = ArticlesGenerator( + context=context, + settings=settings, + path=content_dir, + theme=settings["THEME"], + output_path=None, + ) + generator.generate_context() + + drafts = {article.title: article for article in generator.drafts} + self.assertEqual( + sorted(drafts), + ["Draft with category", "Draft without category"], + ) + # Sanity check: the drafts must be without categories as intended, + # otherwise this test would not exercise the regression. + self.assertFalse(hasattr(drafts["Draft without category"], "category")) + self.assertEqual( + drafts["Draft with category"].category.name, "Test Category" + ) + + write = MagicMock() + # Used to raise AttributeError for the draft without a category. + generator.generate_drafts(write) + + self.assertEqual(write.call_count, len(generator.drafts)) + written = { + call.kwargs["article"]: call.kwargs["category"] + for call in write.call_args_list + } + self.assertIsNone(written[drafts["Draft without category"]]) + self.assertIs( + drafts["Draft with category"].category, + written[drafts["Draft with category"]], + ) + finally: + rmtree(content_dir) + def test_articles_hidden(self): hidden_articles_expected = [ ["Hidden article", "hidden", "Default", "article"],