Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Write `[[Category:Name]]` anywhere in a wiki article to put it in a category. Th
- `[[Category:boxes]]` and `[[Category:Boxes]]` are the same category: the first letter is capitalised and underscores count as spaces.
- **Sort keys:** `[[Category:Boxes|Small]]` files the article under "Small" in that category's list instead of under its title. If an article names a category twice, the first key counts.
- **Linking to a category:** `[[:Category:Boxes]]`, with a colon in front, is a plain link to the category's page and does not put the article in it.
- **Sub-categories:** a category page that is itself in a category (`Category:Extensions` with `[[Category:Software]]` in it) is listed as a sub-category on that category's page, above its articles.
- **Sub-categories:** a category page that is itself in a category (`Category:Extensions` with `[[Category:Software]]` in it) is listed as a sub-category on that category's page, above its articles, with how many articles (and sub-categories) it holds. Sub-categories have their own pages, so moving through them keeps the page of articles, and the other way round.
- **A category page** is the article `Category:Name`, at `/wiki/Category:Name`. If nobody wrote it, the page still exists as a list of the articles in the category, by sort key or title, with their descriptions, with a link to write a description. If an article called `Category:Name` exists, its text is shown above the list, and it has the usual edit and version buttons.
- **`/wiki/categories`** lists every category with a live article in it, how many, and its description (the description field of its `Category:Name` article), linked from the overview. A **How to use categories** box above the list explains `[[Category:Name]]` and category pages, plus, for members who can edit, how to describe a category. Under each category, **Articles in it and how to add one** opens the line to write and its first five articles. Category pages are left out of the overview's article lists.
- Only the live version of an article counts. A forum post keeps `[[Category:Name]]` as plain text, like the rest of the wiki markup.
Expand Down
37 changes: 26 additions & 11 deletions controller/category.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,34 @@ public function __construct(\phpbb\auth\auth $auth, \phpbb\config\config $config
*/
protected function assign_members($name)
{
// Categories that are in this one, by sort key or else name
foreach ($this->categories->subcategories($name) as $row)
$per_page = max(1, (int) $this->config['topics_per_page']);

// Categories that are in this one, by sort key or else name, paged on their own (substart)
// so moving through them keeps the page of articles, and the other way round
$subcategories = $this->categories->subcategories($name);
$sub_total = count($subcategories);
$sub_start = $this->pagination->validate_start($this->request->variable('substart', 0), $per_page, $sub_total);
$members = $this->categories->members($name);
$total = count($members);
$start = $this->pagination->validate_start($this->request->variable('start', 0), $per_page, $total);

$shown = array_slice($subcategories, $sub_start, $per_page);
$counts = $this->categories->counts(array_map(function ($row) {
return substr($row['article_url'], strlen(categories::PREFIX));
}, $shown));
foreach ($shown as $row)
{
$sub_name = categories::normalise(substr($row['article_url'], strlen(categories::PREFIX)));
$count = isset($counts[$sub_name]) ? $counts[$sub_name] : array('articles' => 0, 'subcategories' => 0);
$this->template->assign_block_vars('category_subcategories', array(
'NAME' => substr($row['article_url'], strlen(categories::PREFIX)),
'NAME' => $sub_name,
'DESCRIPTION' => $row['article_description'],
'COUNT' => $this->user->lang('WIKI_CATEGORY_HOLDS_ARTICLES', $count['articles'])
. ($count['subcategories'] ? ', ' . $this->user->lang('WIKI_CATEGORY_HOLDS_SUBCATEGORIES', $count['subcategories']) : ''),
'U_CATEGORY' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $row['article_url'])),
));
}

$members = $this->categories->members($name);
$total = count($members);
$per_page = max(1, (int) $this->config['topics_per_page']);
$start = $this->pagination->validate_start($this->request->variable('start', 0), $per_page, $total);
$this->pagination->generate_template_pagination($this->helper->route('phpbbmodders_wiki_category', array('name' => $name) + ($start ? array('start' => $start) : array())), 'sub_pagination', 'substart', $sub_total, $per_page, $sub_start);

foreach (array_slice($members, $start, $per_page) as $row)
{
Expand All @@ -121,10 +135,11 @@ protected function assign_members($name)
));
}

$this->pagination->generate_template_pagination($this->helper->route('phpbbmodders_wiki_category', array('name' => $name)), 'pagination', 'start', $total, $per_page, $start);
$this->pagination->generate_template_pagination($this->helper->route('phpbbmodders_wiki_category', array('name' => $name) + ($sub_start ? array('substart' => $sub_start) : array())), 'pagination', 'start', $total, $per_page, $start);
$this->template->assign_vars(array(
'S_CATEGORY_PAGE' => true,
'CATEGORY_TOTAL' => $this->user->lang('WIKI_CATEGORY_ARTICLES_TOTAL', $total),
'S_CATEGORY_PAGE' => true,
'CATEGORY_TOTAL' => $this->user->lang('WIKI_CATEGORY_ARTICLES_TOTAL', $total),
'CATEGORY_SUBCATEGORY_TOTAL' => $sub_total ? $this->user->lang('WIKI_CATEGORY_SUBCATEGORIES_TOTAL', $sub_total) : '',
));
}

Expand Down
10 changes: 3 additions & 7 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,9 @@ Built: `[[Category:Name]]` in an article puts it in a category, a category page
(`/wiki/Category:Name`) lists the articles in it, and `/wiki/categories` lists them all. See
the README.

Also built (10/15/2026): categories added by a template, sub-categories, `[[:Category:Name]]`
links and sort keys, from a stored membership table that a cron task fills after the update.

Not built:

- Paging the sub-categories of a category separately from its articles, and showing how many
articles each sub-category holds.
Also built (10/15/2026): categories added by a template, sub-categories (paged on their own,
with how many articles each holds), `[[:Category:Name]]` links and sort keys, from a stored
membership table that a cron task fills after the update. Nothing more is planned here.

## A wiki-style UI, not just reskinned forum templates

Expand Down
13 changes: 13 additions & 0 deletions language/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@
),
'WIKI_CATEGORY_MEMBERS' => 'Articles in this category',
'WIKI_CATEGORY_SUBCATEGORIES' => 'Sub-categories',
'WIKI_CATEGORY_SUBCATEGORIES_TOTAL' => array(
1 => '1 sub-category',
2 => '%d sub-categories',
),
'WIKI_CATEGORY_HOLDS_ARTICLES' => array(
0 => 'no articles',
1 => '1 article',
2 => '%d articles',
),
'WIKI_CATEGORY_HOLDS_SUBCATEGORIES' => array(
1 => '1 sub-category',
2 => '%d sub-categories',
),
'WIKI_CATEGORY_EMPTY' => 'No article is in this category yet.',
'WIKI_CATEGORY_NO_PAGE' => 'This category has no description yet.',
'WIKI_CATEGORY_WRITE' => 'Write one',
Expand Down
24 changes: 23 additions & 1 deletion styles/prosilver/template/category_members.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,31 @@
<h3>{L_WIKI_CATEGORY_SUBCATEGORIES}</h3>
<ul class="wiki-subcategories">
<!-- BEGIN category_subcategories -->
<li><a href="{category_subcategories.U_CATEGORY}"><strong>{category_subcategories.NAME}</strong></a><!-- IF category_subcategories.DESCRIPTION --> &ndash; {category_subcategories.DESCRIPTION}<!-- ENDIF --></li>
<li><a href="{category_subcategories.U_CATEGORY}"><strong>{category_subcategories.NAME}</strong></a> <span class="wiki-subcategory-count">({category_subcategories.COUNT})</span><!-- IF category_subcategories.DESCRIPTION --> &ndash; {category_subcategories.DESCRIPTION}<!-- ENDIF --></li>
<!-- END category_subcategories -->
</ul>
<div class="action-bar">
<div class="pagination">
{CATEGORY_SUBCATEGORY_TOTAL}
<!-- IF .sub_pagination -->
<ul>
<!-- BEGIN sub_pagination -->
<!-- IF sub_pagination.S_IS_PREV -->
<li class="arrow previous"><a class="button button-icon-only" href="{sub_pagination.PAGE_URL}" rel="prev" role="button"><i class="icon fa-chevron-{S_CONTENT_FLOW_BEGIN} fa-fw" aria-hidden="true"></i><span class="sr-only">{L_PREVIOUS}</span></a></li>
<!-- ELSEIF sub_pagination.S_IS_CURRENT -->
<li class="active"><span>{sub_pagination.PAGE_NUMBER}</span></li>
<!-- ELSEIF sub_pagination.S_IS_ELLIPSIS -->
<li class="ellipsis" role="separator"><span>{L_ELLIPSIS}</span></li>
<!-- ELSEIF sub_pagination.S_IS_NEXT -->
<li class="arrow next"><a class="button button-icon-only" href="{sub_pagination.PAGE_URL}" rel="next" role="button"><i class="icon fa-chevron-{S_CONTENT_FLOW_END} fa-fw" aria-hidden="true"></i><span class="sr-only">{L_NEXT}</span></a></li>
<!-- ELSE -->
<li><a class="button" href="{sub_pagination.PAGE_URL}" role="button">{sub_pagination.PAGE_NUMBER}</a></li>
<!-- ENDIF -->
<!-- END sub_pagination -->
</ul>
<!-- ENDIF -->
</div>
</div>
<!-- ENDIF -->
<h3>{L_WIKI_CATEGORY_MEMBERS}</h3>
<!-- IF .category_members -->
Expand Down
6 changes: 6 additions & 0 deletions styles/prosilver/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,9 @@ form.wiki-start-from p {
.wiki-buttons .button.wiki-protect-on .icon {
color: #bc2a4d;
}

/* On a category page: how much each sub-category holds */
.wiki-subcategory-count {
opacity: 0.8;
font-size: 0.95em;
}
22 changes: 22 additions & 0 deletions tests/wiki/category_members_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ public function test_members_sort_by_key_or_title_with_sub_categories_apart()
$this->assertSame(array('Fruit' => 4, 'Tropical' => 1), $members->in_use());
}

public function test_counts_give_articles_and_sub_categories_apart()
{
$members = $this->members(array(
'Mango' => array('Tropical' => ''),
'Apple' => array('Tropical' => ''),
'Category:Tropical' => array('Fruit' => ''),
'Zebra' => array('Fruit' => ''),
'Draft' => array('Tropical' => ''),
));
foreach (array('Mango', 'Apple', 'Category:Tropical', 'Zebra', 'Draft') as $article)
{
$members->update($article);
}

$this->assertSame(array(
'Tropical' => array('articles' => 2, 'subcategories' => 0),
'Fruit' => array('articles' => 1, 'subcategories' => 1),
'Empty' => array('articles' => 0, 'subcategories' => 0),
), $members->counts(array('tropical', 'Fruit', 'Empty')), 'a version that is not live does not count');
$this->assertSame(array(), $members->counts(array()));
}

public function test_an_article_that_is_not_live_is_not_listed()
{
$members = $this->members(array('Draft' => array('Fruit' => '')));
Expand Down
11 changes: 11 additions & 0 deletions wiki/categories.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,17 @@ public function subcategories($category)
return $this->category_members->built() ? $this->category_members->members($category)['subcategories'] : array();
}

/**
* How many articles and sub-categories each of these categories holds.
*
* @param array $categories
* @return array Normalised names with 'articles' and 'subcategories' counts
*/
public function counts(array $categories)
{
return $this->category_members->counts($categories);
}

/**
* The short description of every category that has a page: the description field of
* the live version of its "Category:Name" article.
Expand Down
31 changes: 31 additions & 0 deletions wiki/category_members.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,37 @@ public static function compare(array $a, array $b)
return strnatcasecmp($key_a, $key_b) ?: strcmp($a['article_url'], $b['article_url']);
}

/**
* How many live articles and sub-categories each of these categories holds, read in one
* query, for the counts beside sub-categories.
*
* @param array $categories Category names
* @return array Normalised names with 'articles' and 'subcategories' counts
*/
public function counts(array $categories)
{
$names = array_values(array_unique(array_map(array(categories::class, 'normalise'), $categories)));
$counts = array_fill_keys($names, array('articles' => 0, 'subcategories' => 0));
if (!$names)
{
return $counts;
}

$sql = 'SELECT m.category, a.article_url
FROM ' . $this->member_table . ' m
JOIN ' . $this->article_table . ' a ON a.article_url = m.article_url
WHERE ' . $this->db->sql_in_set('m.category', $names) . '
AND ' . $this->templates->live_where('a', '');
$result = $this->db->sql_query($sql);
while ($row = $this->db->sql_fetchrow($result))
{
$counts[$row['category']][strpos($row['article_url'], categories::PREFIX) === 0 ? 'subcategories' : 'articles']++;
}
$this->db->sql_freeresult($result);

return $counts;
}

/**
* Every category with a live article in it, and how many (sub-categories included).
*
Expand Down
Loading