1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
|
# -*- coding: utf-8 -*-
# Copyright © 2012-2024 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice
# shall be included in all copies or substantial portions of
# the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Render the category pages and feeds."""
import os
from nikola.plugin_categories import Taxonomy
from nikola import utils, hierarchy_utils
class ClassifyCategories(Taxonomy):
"""Classify the posts by categories."""
name = "classify_categories"
classification_name = "category"
overview_page_variable_name = "categories"
overview_page_items_variable_name = "cat_items"
overview_page_hierarchy_variable_name = "cat_hierarchy"
more_than_one_classifications_per_post = False
has_hierarchy = True
include_posts_from_subhierarchies = True
include_posts_into_hierarchy_root = False
show_list_as_subcategories_list = False
template_for_classification_overview = "tags.tmpl"
always_disable_rss = False
always_disable_atom = False
apply_to_posts = True
apply_to_pages = False
minimum_post_count_per_classification_in_overview = 1
omit_empty_classifications = True
add_other_languages_variable = True
path_handler_docstrings = {
'category_index': """A link to the category index.
Example:
link://category_index => /categories/index.html""",
'category': """A link to a category. Takes page number as optional keyword argument.
Example:
link://category/dogs => /categories/dogs.html""",
'category_atom': """A link to a category's Atom feed.
Example:
link://category_atom/dogs => /categories/dogs.atom""",
'category_rss': """A link to a category's RSS feed.
Example:
link://category_rss/dogs => /categories/dogs.xml""",
}
def set_site(self, site):
"""Set site, which is a Nikola instance."""
super().set_site(site)
self.show_list_as_index = self.site.config['CATEGORY_PAGES_ARE_INDEXES']
self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl"
self.translation_manager = utils.ClassificationTranslationManager()
# Needed to undo names for CATEGORY_PAGES_FOLLOW_DESTPATH
self.destpath_names_reverse = {}
for lang in self.site.config['TRANSLATIONS']:
self.destpath_names_reverse[lang] = {}
for k, v in self.site.config['CATEGORY_DESTPATH_NAMES'](lang).items():
self.destpath_names_reverse[lang][v] = k
self.destpath_names_reverse = utils.TranslatableSetting(
'_CATEGORY_DESTPATH_NAMES_REVERSE', self.destpath_names_reverse,
self.site.config['TRANSLATIONS'])
def is_enabled(self, lang=None):
"""Return True if this taxonomy is enabled, or False otherwise."""
return True
def classify(self, post, lang):
"""Classify the given post for the given language."""
cat = post.meta('category', lang=lang).strip()
return [cat] if cat else []
def get_classification_friendly_name(self, classification, lang, only_last_component=False):
"""Extract a friendly name from the classification."""
classification = self.extract_hierarchy(classification)
return classification[-1] if classification else ''
def get_overview_path(self, lang, dest_type='page'):
"""Return a path for the list of all classifications."""
if self.site.config['CATEGORIES_INDEX_PATH'](lang):
path = self.site.config['CATEGORIES_INDEX_PATH'](lang)
append_index = 'never'
else:
path = self.site.config['CATEGORY_PATH'](lang)
append_index = 'always'
return [component for component in path.split('/') if component], append_index
def slugify_tag_name(self, name, lang):
"""Slugify a tag name."""
if self.site.config['SLUG_TAG_PATH']:
name = utils.slugify(name, lang)
return name
def slugify_category_name(self, path, lang):
"""Slugify a category name."""
if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']:
path = path[-1:] # only the leaf
result = [self.slugify_tag_name(part, lang) for part in path]
result[0] = self.site.config['CATEGORY_PREFIX'] + result[0]
if not self.site.config['PRETTY_URLS']:
result = ['-'.join(result)]
return result
def get_path(self, classification, lang, dest_type='page'):
"""Return a path for the given classification."""
cat_string = '/'.join(classification)
classification_raw = classification # needed to undo CATEGORY_DESTPATH_NAMES
destpath_names_reverse = self.destpath_names_reverse(lang)
if self.site.config['CATEGORY_PAGES_FOLLOW_DESTPATH']:
base_dir = None
for post in self.site.posts_per_category[cat_string]:
if post.category_from_destpath:
base_dir = post.folder_base(lang)
# Handle CATEGORY_DESTPATH_NAMES
if cat_string in destpath_names_reverse:
cat_string = destpath_names_reverse[cat_string]
classification_raw = cat_string.split('/')
break
if not self.site.config['CATEGORY_DESTPATH_TRIM_PREFIX']:
# If prefixes are not trimmed, we'll already have the base_dir in classification_raw
base_dir = ''
if base_dir is None:
# fallback: first POSTS entry + classification
base_dir = self.site.config['POSTS'][0][1]
base_dir_list = base_dir.split(os.sep)
sub_dir = [self.slugify_tag_name(part, lang) for part in classification_raw]
return [_f for _f in (base_dir_list + sub_dir) if _f], 'auto'
else:
return [_f for _f in [self.site.config['CATEGORY_PATH'](lang)] if _f] + self.slugify_category_name(
classification, lang), 'auto'
def extract_hierarchy(self, classification):
"""Given a classification, return a list of parts in the hierarchy."""
return hierarchy_utils.parse_escaped_hierarchical_category_name(classification)
def recombine_classification_from_hierarchy(self, hierarchy):
"""Given a list of parts in the hierarchy, return the classification string."""
return hierarchy_utils.join_hierarchical_category_path(hierarchy)
def provide_overview_context_and_uptodate(self, lang):
"""Provide data for the context and the uptodate list for the list of all classifiations."""
kw = {
'category_path': self.site.config['CATEGORY_PATH'],
'category_prefix': self.site.config['CATEGORY_PREFIX'],
"category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'],
"tzinfo": self.site.tzinfo,
"category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'],
"category_titles": self.site.config['CATEGORY_TITLES'],
}
context = {
"title": self.site.MESSAGES[lang]["Categories"],
"description": self.site.MESSAGES[lang]["Categories"],
"pagekind": ["list", "tags_page"],
"category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'](lang),
"category_titles": self.site.config['CATEGORY_TITLES'](lang),
}
kw.update(context)
return context, kw
def provide_context_and_uptodate(self, classification, lang, node=None):
"""Provide data for the context and the uptodate list for the list of the given classifiation."""
cat_path = self.extract_hierarchy(classification)
kw = {
'category_path': self.site.config['CATEGORY_PATH'],
'category_prefix': self.site.config['CATEGORY_PREFIX'],
"category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'],
"tzinfo": self.site.tzinfo,
"category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'],
"category_titles": self.site.config['CATEGORY_TITLES'],
}
posts = self.site.posts_per_classification[self.classification_name][lang]
if node is None:
children = []
else:
children = [child for child in node.children if len([post for post in posts.get(child.classification_name, []) if self.site.config['SHOW_UNTRANSLATED_POSTS'] or post.is_translation_available(lang)]) > 0]
subcats = [(child.name, self.site.link(self.classification_name, child.classification_name, lang)) for child in children]
friendly_name = self.get_classification_friendly_name(classification, lang)
context = {
"title": self.site.config['CATEGORY_TITLES'](lang).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % friendly_name),
"description": self.site.config['CATEGORY_DESCRIPTIONS'](lang).get(classification),
"pagekind": ["tag_page", "index" if self.show_list_as_index else "list"],
"tag": friendly_name,
"category": classification,
"category_path": cat_path,
"subcategories": subcats,
}
kw.update(context)
return context, kw
def get_other_language_variants(self, classification, lang, classifications_per_language):
"""Return a list of variants of the same category in other languages."""
return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
"""Rearrange, modify or otherwise use the list of posts per classification and per language."""
self.translation_manager.read_from_config(self.site, 'CATEGORY', posts_per_classification_per_language, False)
def should_generate_classification_page(self, classification, post_list, lang):
"""Only generates list of posts for classification if this function returns True."""
if self.site.config["CATEGORY_PAGES_FOLLOW_DESTPATH"]:
# In destpath mode, allow users to replace the default category index with a custom page.
classification_hierarchy = self.extract_hierarchy(classification)
dest_list, _ = self.get_path(classification_hierarchy, lang)
short_destination = os.sep.join(dest_list + [self.site.config["INDEX_FILE"]])
if short_destination in self.site.post_per_file:
return False
return True
def should_generate_atom_for_classification_page(self, classification, post_list, lang):
"""Only generates Atom feed for list of posts for classification if this function returns True."""
return True
def should_generate_rss_for_classification_page(self, classification, post_list, lang):
"""Only generates RSS feed for list of posts for classification if this function returns True."""
return True
|