Permalink
Browse files

pelican_04_category: create the category template

  • Loading branch information...
jaredandrews committed Feb 8, 2017
1 parent 8032582 commit 575951007462514058cbcb171f2286ebcbbc147a
@@ -0,0 +1,4 @@
Title: Making This Site
Date: 2015-11-01 10:02

A flow of conscious tutorial describing, in excruciating detail, how this site was made.
@@ -1,7 +1,6 @@
Title: Making This Site , Part 0: Setup
Date: 2015-11-01 10:02
Tags: programming, web-dev, pelican
Category: making-this-site

Welcome to the first installment of "Making This Site". In these articles I will describe the exact steps I went through to build this site.

@@ -1,7 +1,6 @@
Title: Making This Site, Part 1: Base Template
Date: 2015-11-03 19:00
Tags: programming, web-dev, pelican, skeleton, jinja2
Category: making-this-site

Welcome to the first installment of "Making This Site". In these articles I will describe the exact steps I went through to build this site.

@@ -1,7 +1,6 @@
Title: Making This Site, Part 2: Index Template
Date: 2017-02-04 19:00
Tags: programming, web-dev, pelican
Category: making-this-site

Welcome to the second installment of "Making This Site". In these articles I will describe the exact steps I went through to build this site.

@@ -1,7 +1,6 @@
Title: Making This Site, Part 3: Article Template
Date: 2017-02-04 21:36
Tags: programming, web-dev, pelican, jinja2
Category: making-this-site

Welcome to the third installment of "Making This Site". In these articles I will describe the exact steps I went through to build this site.

@@ -0,0 +1,136 @@
Title: Making This Site, Part 4: Category Template
Date: 2017-02-07 21:36
Tags: programming, web-dev, pelican, jinja2

Welcome to the fourth installment of "Making This Site". In these articles I will describe the exact steps I went through to build this site.

### The Category Template

`category.html` is a template for showing all the articles in a specific category. For my purposes I would like to show the name of the category, a description and a time ordered list of all the posts associated with it.

Lets get started:

$ touch theme/templates/category.html

Now we can open up our new template and add some boilerplate:

{% extends "base.html" %}
{% block title %}{{ category }} — {{ SITENAME }}{% endblock %}
{% block content %}
<h2>{{ category }}</h2>
<!-- TODO - content -->
{% endblock %}

The category template is has it's own [set of variables](http://docs.getpelican.com/en/stable/themes.html#category-html). Above we use the `category` variable to print the category name in both the `<title>` block and in the content of the page.

#### Listing Articles in a Category

I want to list the articles in a category in the same way that I do on the home page. This opens up another codesharing opportunity, let's take the the article listing code in `index.html`, modify it and put it in `macros.html`.

{% macro get_article_list(articles, default_category) %}
<ul>
{% for article in articles %}
<li>
<a href="{{ SITEURL }}/{{ article.url }}">{{ article.title }}</a>
&nbsp
<span class="post-meta">({{ get_meta_data_html(article, default_category) }})</span>
</li>
{% endfor %}
</ul>
{% endmacro %}

Because of the dependence on the `get_meta_data_html` macro we also need to pass `DEFAULT_CATEGORY` to `get_article_list`. To make use of this new macro, add code to import it at the top of the file:

{% from 'macros.html' import get_article_list %}

And then replace `<!-- TODO - content --> with:
{{ get_article_list(articles, DEFAULT_CATEGORY) }}
`articles` is another template variable that provides a list of articles associated with a category.
#### Adding a Category Description With category_meta
At this point the category page is almost exactly how I want it. The only other thing I desire is a description of the category which will be printed above the list of articles. A category description is not supported by Pelican out of the box.
With a quick Google search I found the [category_meta](https://github.com/getpelican/pelican-plugins/tree/master/category_meta) plugin which allegedly provided a way to add a description to a Category.
#### Setting Up category_meta
I copied the `category_meta` project into my `plugins` folder.
`category_meta` calls for your posts to be organized such that there is a directory for each category and all the posts associated with that category are in that directory. Thus my `content` folder went from looking like this:
content
├── pelican_00_setup.md
├── pelican_01_base.md
├── pelican_02_index.md
└── pelican_03_article.md
to:
content
└── making-this-site
├── pelican_00_setup.md
├── pelican_01_base.md
├── pelican_02_index.md
├── pelican_03_article.md
└── pelican_04_category.md
The name of this new directory is also what is used for the category page slug. Thus the categeory page for "Making My Site" will by `categories/making-this-site.html`.
`category_meta` also specifies that every category folder contains an `index.md`, this file holds the metadata for the category:
Title: Making This Site
A flow of conscious tutorial describing, in excruciating detail, how this site was made.
Going back to `category.html` we can show the description underneath the title with:
<p>{{ category.description }}</p>
The documentation also says to remove the category key from posts, so I did that as well.
Now we should have category descriptions!
##### Time For a Side Quest: Fixing The category_meta Plugin
After adding all the `category_meta` related files I went and ran `./develop_server start`. The site compiled and I went to check out my sweet new category page. Everything looked good but my category description was missing!
I went back and looked at the output of `develop_server` and saw:
ERROR: Skipping category/index.md: could not find information about 'date'
ERROR: No category assignment for ~/pelican_category_meta_problems/content/category/post.md (~/pelican_category_meta_problems/content/category/post.md)
Hmm okay... It didn't really make sense but I went ahead and added a date to `making-this-site/index.md`.
Date: 2015-11-01 10:02
Building the again a new error appeared:
CRITICAL: AttributeError: can't set attribute
Since I have the source of the plugin in my repo I was able to trace this warning back into the `category_meta` plugin. In `plugins/category_meta/category_meta.py` on line 73, there is this piece of code:
category.slug = slug
First I just commented it out. This change got my category description to appear but I didn't feel good about it, obviously that line was there for a reason, right?!
From what I can tell the purpose of that line is to set the categories slug to the name of the categories directory name. Line 73 is part of the function `make_category(article, slug)`, and indeed, it is called in `pretaxonomy_hook` function like this:
make_category(article, os.path.basename(dirname))
By removing line 73 I would lose the ability to set the category slug based on the directory and I'm not even sure how the category slug would be generated. I didn't did into the Pelican source enough but from what I can tell it took the name of the category and snakecased.
So what could have caused this issue? At the time of writing this article I am using the newest vesrion of Pelican, `3.7.1`. My guess woulld be that at some point `category.slug` was mutable and in this version it is not. I inspected the `category` object to see if I could edit the slug in another way. Running `dir(category)` revealed that there was another member of `category` called `_slug`, so I changed line 73 to:
category._slug = slug
This fixed the issue! BUT I had now modified the plugin to access what, by Python convention, is supposed to be a private variable. The danger in doing this is that the variable could disappear or change next time I upgrade Pelican.
This made me feel bad but I have a website to build! Thus I documented and reported the issue to [pelican-plugins on GitHub](https://github.com/getpelican/pelican-plugins), you can see the issue [here](https://github.com/getpelican/pelican-plugins/issues/855). Hopefully, by the time this article is published there will be a cleaner solution than what I have done above.
### Wrapping Up
After al that, we now have our own Category template!
To see the complete code for the site at this point checkout COMMIT HASH LINK on GitHub.
@@ -30,4 +30,5 @@
STATIC_PATHS = ['images']

PLUGIN_PATHS = ['plugins']
PLUGINS = ['neighbors']
PLUGINS = ['neighbors', 'category_meta']

@@ -0,0 +1,22 @@
Category Metadata
-----------------

A plugin to read metadata for each category from an index file in that
category's directory.

For this plugin to work properly, your articles should not have a
Category: tag in their metadata; instead, they should be stored in
(subdirectories of) per-category directories. Each per-category
directory must have a file named 'index.ext' at its top level, where
.ext is any extension that will be picked up by an article reader.
The metadata of that article becomes the metadata for the category,
copied over verbatim, with three special cases:

* The category's name is set to the article's title.
* The category's slug is set to the name of the parent directory
of the index.ext file.
* The _text_ of the article is stored as category.description.

You can use this, for example, to control the slug used for each
category independently of its name, or to add a description at the top
of each category page.
@@ -0,0 +1 @@
from .category_meta import *
@@ -0,0 +1,130 @@
'''Copyright 2014, 2015 Zack Weinberg
Category Metadata
-----------------
A plugin to read metadata for each category from an index file in that
category's directory.
For this plugin to work properly, your articles should not have a
Category: tag in their metadata; instead, they should be stored in
(subdirectories of) per-category directories. Each per-category
directory must have a file named 'index.ext' at its top level, where
.ext is any extension that will be picked up by an article reader.
The metadata of that article becomes the metadata for the category,
copied over verbatim, with three special cases:
* The category's name is set to the article's title.
* The category's slug is set to the name of the parent directory
of the index.ext file.
* The _text_ of the article is stored as category.description.
'''

from pelican import signals
import os
import re

import logging
logger = logging.getLogger(__name__)

### CORE BUG: https://github.com/getpelican/pelican/issues/1547
### Content.url_format does not honor category.slug (or author.slug).
### The sanest way to work around this is to dynamically redefine each
### article's class to a subclass of itself with the bug fixed.
###
### Core was fixed in rev 822fb134e041c6938c253dd4db71813c4d0dc74a,
### which is not yet in any release, so we dynamically detect whether
### the installed version of Pelican still has the bug.

patched_subclasses = {}
def make_patched_subclass(klass):
if klass.__name__ not in patched_subclasses:
class PatchedContent(klass):
@property
def url_format(self):
metadata = super(PatchedContent, self).url_format
if hasattr(self, 'author'):
metadata['author'] = self.author.slug
if hasattr(self, 'category'):
metadata['category'] = self.category.slug

return metadata
# Code in core uses Content class names as keys for things.
PatchedContent.__name__ = klass.__name__
patched_subclasses[klass.__name__] = PatchedContent
return patched_subclasses[klass.__name__]

def patch_urlformat(cont):
# Test whether this content object needs to be patched.
md = cont.url_format
if ((hasattr(cont, 'author') and cont.author.slug != md['author']) or
(hasattr(cont, 'category') and cont.category.slug != md['category'])):
logger.debug("Detected bug 1547, applying workaround.")
cont.__class__ = make_patched_subclass(cont.__class__)

### END OF BUG WORKAROUND

def make_category(article, slug):
# Reuse the article's existing category object.
category = article.category

# Setting a category's name resets its slug, so do that first.

category.name = article.title
category._slug = slug

# Description from article text.
# XXX Relative URLs in the article content may not be handled correctly.
setattr(category, 'description', article.content)

# Metadata, to the extent that this makes sense.
for k, v in article.metadata.items():
if k not in ('path', 'slug', 'category', 'name', 'title',
'description', 'reader'):
setattr(category, k, v)

logger.debug("Category: %s -> %s", category.slug, category.name)
return category

def pretaxonomy_hook(generator):
"""This hook is invoked before the generator's .categories property is
filled in. Each article has already been assigned a category
object, but these objects are _not_ unique per category and so are
not safe to tack metadata onto (as is).
The category metadata we're looking for is represented as an
Article object, one per directory, whose filename is 'index.ext'.
"""

category_objects = {}
real_articles = []

for article in generator.articles:
dirname, fname = os.path.split(article.source_path)
fname, _ = os.path.splitext(fname)
if fname == 'index':
category_objects[dirname] = \
make_category(article, os.path.basename(dirname))
else:
real_articles.append(article)

category_assignment = \
re.compile("^(" +
"|".join(re.escape(prefix)
for prefix in category_objects.keys()) +
")/")

for article in real_articles:
m = category_assignment.match(article.source_path)
if not m or m.group(1) not in category_objects:
logger.error("No category assignment for %s (%s)",
article, article.source_path)
continue

article.category = category_objects[m.group(1)]
patch_urlformat(article)

generator.articles = real_articles

def register():
signals.article_generator_pretaxonomy.connect(pretaxonomy_hook)
@@ -4,7 +4,7 @@
{% block content %}
<h2 class="article-title">{{ article.title }}</h1>
<footer class="article-metadata">
{{ get_meta_data_html(article, DEFAULT_CATEGORY) }}
{{ get_meta_data_html(article) }}
</footer>
<div class="article-body">{{ article.content }}</div>
{% if article.category != DEFAULT_CATEGORY %}
@@ -0,0 +1,8 @@
{% from 'macros.html' import get_article_list %}
{% extends "base.html" %}
{% block title %}{{ category }} — {{ SITENAME }}{% endblock %}
{% block content %}
<h2>{{ category }}</h2>
<p>{{ category.description }}</p>
{{ get_article_list(articles, DEFAULT_CATEGORY) }}
{% endblock %}
@@ -1,13 +1,5 @@
{% from 'macros.html' import get_meta_data_html %}
{% from 'macros.html' import get_article_list %}
{% extends "base.html" %}
{% block content %}
<ul>
{% for article in articles_page.object_list %}
<li>
<a href="{{ SITEURL }}/{{ article.url }}">{{ article.title }}</a>
&nbsp
<span class="post-meta">({{ get_meta_data_html(article, DEFAULT_CATEGORY) }})</span>
</li>
{% endfor %}
</ul>
{{ get_article_list(articles_page.object_list, DEFAULT_CATEGORY) }}
{% endblock content %}
@@ -1,6 +1,6 @@
{% macro get_meta_data_html(article, default_category) %}
{{ article.date.strftime('%b %d, %Y') }}
{% if article.category != default_category %}
{% if article.category != default_category %}
/ <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>
{% endif %}
{% if article.tags %}
@@ -13,4 +13,16 @@
{% endif %}
{% endfor %}
{% endif %}
{% endmacro %}
{% endmacro %}

{% macro get_article_list(articles, default_category) %}
<ul>
{% for article in articles %}
<li>
<a href="{{ SITEURL }}/{{ article.url }}">{{ article.title }}</a>
&nbsp
<span class="post-meta">({{ get_meta_data_html(article, default_category) }})</span>
</li>
{% endfor %}
</ul>
{% endmacro %}

0 comments on commit 5759510

Please sign in to comment.