Extension:Taxonomy

From MediaWiki.org
Jump to navigation Jump to search
MediaWiki extensions manual
Crystal Clear app error.svg
Taxonomy
Release status: unstable
Implementation Parser function, Tag, Database
Description Provides hierarchical taxonomy for article management
Author(s) Eko Mursito Budi (Mursitotalk)
Latest version 0.9 (2009/07/30)
MediaWiki 1.15
License GPL
Download No link
Example http://computational.engineering.or.id/Taxonomy:Utama
Tags
taxonomy-tree, taxonomy
Hooks used
ArticleSaveComplete
Translate the Taxonomy extension if it is available at translatewiki.net
Check usage and version matrix.

What can this extension do?[edit]

To manage the structure of the articles, MediaWiki uses categories, which excels at the web structure. It can also be used to represent hierarchies, using internal links and subcategories, but it has some weakness:

  • The hierarchy must be constructed in bottom up fashion.
  • Selecting all articles belonging to a top category is slow.

This extension solve the problems by :

  • Adding a new table for storing the taxonomy in nested hierarchy structure.
  • Providing a taxonomy-tree tag to define the taxonomy tree in a page using a top down approach.
  • Providing a taxonomy tag to show the dynamic article list in any page, using the faster nested hierarchy technique.

Usage[edit]

After installation (see below), there are three general steps to use it.

Define The Taxonomy Tree[edit]

  • Create a new taxonomy page (e.g: Taxonomy:Main)
  • In the page, write the taxonomy definition, for example :

This is the definition of "Main" taxonomy.
<taxonomy-tree show=default>
* Information Technology
** Hardware
*** Computer
*** Networking
** Software
*** Operating System
*** Middle ware
*** Applications
</taxonomy-tree>

Note that each line contains the bulleted tags (*) to express the hierarchy depth, then the category name. When the page is saved, the extension creates the standard MediaWiki categories as needed (including the Main category). However, it won't necessarily create the category page. (Technically speaking, it insert new records in the 'taxonomy' and 'category' tables, but not in the 'page' nor in the 'categorylinks' table).

The taxonomy-tree tag support the following parameters:

parameter possible values description
show default, hide show or hide the tree when rendered

Attach Pages to the Categories[edit]

Attach any page to the category using the standard MediaWiki tag, e.g:

[[Category:Hardware]]
[[Category:Operating System]]

Show the dynamic page list[edit]

In any page, just add the taxonomy tag as follow:

<taxonomy type=hot category=Hardware count=10>Main</taxonomy>

or use the parser instead (recommended for a template):

{{#tag:taxonomy | Main | type=new | count=5 | category=Hardware }}

The following parameters might be used

parameter possible values description
category one of the category name parent category to be listed
count integer number number of pages listed
type hot, new show the hottest articles, or the newest articles.
sort asc, desc show the articles in ascending or descending order

Download instructions[edit]

The source code can be found below.

Installation[edit]

This extension requires 4 steps of installation.

Add the Taxonomy Table[edit]

Please create the taxonomy table in your Mediawiki database, using the following SQL code (This is for MySQL database).

CREATE TABLE IF NOT EXISTS `taxonomy` (
  `tax_page` int(10) NOT NULL,
  `tax_category` int(10) NOT NULL,
  `tax_parent` int(10) NOT NULL,
  `tax_left` int(11) NOT NULL,
  `tax_right` int(11) NOT NULL,
  PRIMARY KEY  (`tax_page`,`tax_category`),
  KEY `tax_parent` (`tax_parent`),
  KEY `tax_left` (`tax_left`),
  KEY `tax_right` (`tax_right`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Put the Taxonomy.php file[edit]

Copy the Taxonomy.php source code below, and place it in $IP/extensions/Taxonomy/Taxonomy.php. Note: $IP stands for the root directory of your MediaWiki installation, the same directory that holds LocalSettings.php.

Put the Taxonomy.i18n.php file[edit]

Copy the Taxonomy.i18n.php source code below, and place it in $IP/extensions/Taxonomy/Taxonomy.i18n.php. If necessary, edit that file to add your localization.

Edit the LocalSettings.php[edit]

Add the following lines

# Define a new Taxonomy name space
# Fell free to adjust the namespace number as appropriate, but keep it forever
define("NS_TAXONOMY", 1000);
define("NS_TAXONOMY_TALK", 1001);
$wgExtraNamespaces[NS_TAXONOMY] = "Taxonomy";
$wgExtraNamespaces[NS_TAXONOMY_TALK] = "Taxonomy_talk";

# Include the Taxonomy extension
require_once("$IP/extensions/Taxonomy/Taxonomy.php");

Code[edit]

Taxonomy.php[edit]

<?php
/*
 Taxonomy 
 MediaWiki extension for managing articles hierarchically
 * @ingroup Extensions
 * @author Eko M. Budi

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along
 with this program; if not, write to the Free Software Foundation, Inc.,
 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 http://www.gnu.org/copyleft/gpl.html

Revision:

2009/06/28 : version 0.1
- Internasionalisation for EN and ID
- taxonomy tag hook, support type={hot|new}
- taxonomy-tree tag hook, support show={hide|default}
- taxonomy-tree articlesavecomplete hook, support saving to the table
- database, define a new taxonomy table, links to category, categorylinks, and page
*/

//Avoid unstubbing $wgParser too early on modern (1.12+) MW versions, as per r35980
if ( defined( 'MW_SUPPORTS_PARSERFIRSTCALLINIT' ) ) {
	$wgHooks['ParserFirstCallInit'][] = 'wfTaxonomyInit';
} else {
	$wgExtensionFunctions[] = 'wfTaxonomyInit';
}

//$wgHooks['UnknownAction'][] = 'actionCreate';
$wgExtensionCredits['parserhook'][] = array(
	'path'           => __FILE__,
	'name'           => 'Taxonomy',
	'url'            => 'http://www.mediawiki.org/wiki/Extension:Taxonomy',
	'description'    => 'Taxonomy of categories using nested hierarchy',
	'author'         => 'Eko M. Budi',
	'version'        => '1.0',
	'descriptionmsg' => 'taxonomy-desc',
);

$dir = dirname(__FILE__) . '/';
$wgExtensionMessagesFiles['Taxonomy'] = $dir . 'Taxonomy.i18n.php';

// for debugging from save hook
function debug($msg) {
    $myFile = 'debug.log';
    $fh = fopen($myFile, 'a') or die("can't open file");
    fwrite($fh, $msg . "\n");
    fclose($fh);
}

// Register on Save hook
$wgHooks['ArticleSaveComplete'][] = 'wfTaxonomySaveCompleteHook';
function wfTaxonomyInit() {
    // register messages
    // global $wgMessageCache, $wgHierarchyMessages;

    // register the extension with the WikiText parser
    global $wgParser;
    $wgParser->setHook( "taxonomy-tree", "wfTaxonomyTreeHook" );
    $wgParser->setHook( "taxonomy", "wfTaxonomyHook" );
    
    return true;
}

// called whe an article is saved
function wfTaxonomySaveCompleteHook(&$article, &$user, $text, $summary, $minoredit, $watchthis, $sectionanchor, $flags) {
    // if not in the TAXONOMY namespace, don't bother
    $namespace = $article->mTitle->getNamespace();
    //debug("SaveCompleteHook " . $article->getTitle() . "(" . $namespace . ")" . NS_TAXONOMY);
    if (($namespace !== NS_TAXONOMY) && ($namespace !== NS_CATEGORY))
	return true;

    // search for a <taxonomy-tree> tag
    // a category article may contain only one taxonomy-tree tag
    $pattern = '@<taxonomy-tree>(.*?)</taxonomy-tree>@is';
    if (preg_match($pattern, $text, $matches, PREG_OFFSET_CAPTURE)) {
	// found one, lets make a taxonomy
        $text = $matches[1][0];
        $tt = TaxonomyTree::newFromText($article, $text);
        $tt->saveTree();
    }
    else {
	// no taxonomy-tree, just make sure to delete one belongs to the article
	TaxonomyTree::removeTree($article);
    }
    return false;
}

// called when an article is rendered and contains the <taxonomy-tree> tag
// Render each item to a Category link
function wfTaxonomyTreeHook ($input, $args, $parser) {
    
    // get the arguments
    $show = 'default';
    if ( $args['show'] === 'hide' )
	$show = 'hide';
	
    switch ($show) {
	case 'hide' : break;
	default: return TaxonomyTreeRenderer::renderDefault($input, $parser);
    }
    return true;
}


function wfTaxonomyHook($input, $args, $parser) {
    $parser->disableCache();
    wfLoadExtensionMessages('Taxonomy');

    // get the root, must be one, bailout if not specified
    if( !empty($args['root'])) {
	$root = $args['root'];
    }
    else if(!empty($input)) {
	$root=$input;
    }
    else {
	return wfMsgHtml('taxonomy-no-root');
    }
    
    // get the other arguments
    $category=$root;
    if (! empty($args['category'] ))
	$category=$args['category'];
    $type = 'new';
    if ( in_array($args['type'], array('new','hot')) )
	$type = $args['type'];

    $sort = 'DESC';
    /*if ( in_array($type, array('hot')) )
	$sort = 'ASC';*/
    
    if ( in_array($args['sort'], array('desc', 'descending')) )
	$sort = 'DESC';
    else if ( in_array($args['sort'], array('asc', 'ascending')) )
	$sort = 'ASC';

    $count = 5;
    if (is_numeric($args['count']))
	$count = $args['count'];
    $title = '';
    if ( ! empty($args['title']))
	$title = $args['title'];
    if ( ! empty($args['namespace']))
	$namespace = $args['namespace'];
    //print("Debug args: $root | $category | $namespace | $type | $count | $sort  \n ");

    // do the initialization
    $t=Title::newFromText($root, NS_CATEGORY);
    $root=$t->getDBkey();
    $t=Title::newFromText($category, NS_CATEGORY);
    $category=$t->getDBkey();
    if (empty($namespace))
	$namespace = NS_MAIN;
    // else, find the right NS

    $dbr =& wfGetDB( DB_SLAVE );
    $root_id = Taxonomy::getRootId($dbr, $root);
    if (empty($root_id)) {
	return "<p>" . wfMsgHtml('taxonomy-no-taxonomy')  . "<p>";
    }
    //print("Debug Root : $root # $root_id\n");
	
    // find the $category
    $tax = Taxonomy::getTaxonomy($dbr, $root_id, $category);
    if (empty($tax)) {
	return "<p>" . wfMsgHtml('taxonomy-no-category') . "<p>";
    }
    //print_r($tax);

    $tr = new TaxonomyRenderer();
    switch ($type) {
	case 'hot' : return $tr->renderHot($dbr, $tax, $namespace, $count, $sort, $title); break;
	case 'new' : return $tr->renderNew($dbr, $tax, $namespace, $count, $sort, $title); break;
	case 'update' : return $tr->renderNew($dbr, $tax, $namespace, $count, $sort, $title); break;
    }

}



/******************************************************************
 * Classes Implementation
 ******************************************************************/
class TaxonomyRenderer {

    // render newest articles below a category
    function renderNew($dbr, $tax, $namespace, $count, $sort, $title) {
	// find all pages under the category
	$list = Taxonomy::getNewArticles($dbr, $tax, $namespace, $count, $sort);
	if (empty ($list))
	    return "<p>" . wfMsgHtml('taxonomy-no-article') . "<p>";
	return $this->renderList($title, $list);
    }    

    function renderHot($dbr, $tax, $namespace, $count, $sort, $title) {
	// find all pages under the category
	$list = Taxonomy::getHotArticles($dbr, $tax, $namespace, $count, $sort);
	if (empty($list)) 
	    return "<p>" . wfMsgHtml('taxonomy-no-article') . "<p>"; 
	return $this->renderList($title, $list);
    }

    function renderList($title, $list) {
	global $wgUser;
	global $wgContLang;
	$sk =& $wgUser->getSkin();

	$output = "<b>$title</b>\n\n";
	foreach ($list as $item) {
	    $t = Title::makeTitle($item->namespace, $item->title);
            $l = $sk->makeKnownLinkObj($t, $wgContLang->convertHtml($t->getText()));
	    $output .= "* " . $l . " <font size=-2>(" . $item->note . ")</font>\n";
	}
	return $output;
    }    
}
 
 
class TaxonomyTreeRenderer {
    static function renderDefault($input, &$parser) {
	$output='';
	$tmp = preg_replace('/([\#|\*]+) *(.*?) *[\r\n|\r|\n]/', '$1[[:Category:$2|$2]]' . "\n", $input);
	/* for debugging
	$output .= '<pre>';
	$output .= $tmp;
	$output .= '</pre>';
	*/
	$localParser = new Parser();
	$tmp = $localParser->parse($tmp, $parser->mTitle, $parser->mOptions);
	$output .= $tmp->getText();
	return $output;    
    }

}


#----------------------------------------------------------------------------
#    TaxonomyItem class for extension
#----------------------------------------------------------------------------
class TaxonomyItem {
    var $tax_page;
    var $tax_category;
    var $tax_parent;
    var $tax_left;
    var $tax_right;
    var $category;

    public static function newTop($cat_id, $page_id) {
	$item = new self();
	$item->tax_page = $page_id;
	$item->tax_category = $cat_id;
	$item->tax_parent = 0;
	$item->tax_left = 1;
	$item->tax_right = 2;
	return $item;
    }

    public static function newChild($cat_id, &$parent) { 
	$item = new self();
	$item->tax_category = $cat_id;
	$item->tax_page = $parent->tax_page;
	$item->tax_parent = $parent->tax_category;
	$item->tax_left = $parent->tax_right;
	$item->tax_right = $parent->tax_right+1;
	return $item;
    }

    public function saveItem($dbw) {
	$dbw->insert(
	    'taxonomy',
	    array( 'tax_page' => $this->tax_page,
		 'tax_category' => $this->tax_category,
		 'tax_parent' => $this->tax_parent,
		 'tax_left' => $this->tax_left,
		 'tax_right' => $this->tax_right),
		__METHOD__,
		'IGNORE'
	);
	return true;
    }
}


class TaxonomyTree {

var $tree; // array to store the nested tree structure

    public static function newFromText($article, $text) {
        preg_match_all('/([\#|\*]+) *(.*?) *[\r\n|\r|\n]/', $text, $matches, PREG_SET_ORDER);
        if (empty ($matches)) return null;
        
	$tax = new self();
	$page_id = $article->getId();
	$top_id = $tax->getCategoryId($article->getTitle());
	//debug("Top : " . $article->getTitle() . " # $top_id | $page_id\n");
	$tax->tree = array();
	$tax->tree[] = TaxonomyItem::newTop($top_id, $page_id);
	//$top = $tax->tree[0];
	//debug("Top : $top->tax_page | $top->tax_category | \n");
	$tax->traverse(1, $matches, $tax->tree);
	return $tax;
    }

    function traverse($level, &$list, &$tree) {
	$top = $tree[0];
	//debug("Top : $top->tax_page | $top->tax_category | \n");
	$index = count($tree)-1;
	$parent = $tree[$index];
	//debug("Parent ($index): $parent->tax_page | $parent->tax_category | $parent->tax_left | $parent->tax_right |\n");
	while ($index <= count($list)) {
	    $len = strlen($list[$index][1]);
	    if ($len < $level) break;
	    if ($len == $level) {
		$catTitle=Title::newFromText($list[$index][2], NS_CATEGORY);
		$catId = $this->getCategoryId($catTitle);
		//debug("$catTitle # $catId \n");
		$current = TaxonomyItem::newChild($catId, $parent);
		//debug("Item : $current->tax_page | $current->tax_category | $current->tax_left | $current->tax_right |\n");
		$parent->tax_right = $current->tax_right+1;
		$tree[]=$current;
	    }
	    else {
		$this->traverse($len, $list, $tree);
		if (isset($current)) {
		    $parent->tax_right=$current->tax_right+1;
		}
	    }
	    $index = count($tree)-1;
	}
	$parent->tax_right=$current->tax_right+1;
	return true;
    }

    function addCategory($dbw, $cat_title) {
	$values = array('cat_title' => $cat_title );
        $dbw->insert('category', $values, __METHOD__, 'IGNORE');
    }

    function getCategory($dbr, $cat_title) {
	$fields = array( '*');
	$where = array( 'cat_title' => $cat_title );
	$row = $dbr->selectRow('category', $fields, $where, __METHOD__);
	return $row;
    }

    // get the category from category table
    // if not exist, create a new one
    function getCategoryId($ctitle) {
	$dbr=wfGetDB( DB_SLAVE );
        $cat_title = $ctitle->getDBkey();
        $cat = $this->getCategory($dbr, $cat_title);
        
        if (! empty($cat) ) {
    	    return $cat->cat_id;
        }
        
	$dbw=wfGetDB( DB_MASTER );
	$dbw->begin();
        $this->addCategory($dbw, $cat_title);
	$dbw->commit();
        
        $cat = $this->getCategory($dbr, $cat_title);
        return $cat->cat_id;
    }
    
    static function deleteTree($dbw, $page_id) {
        $dbw->delete('taxonomy',
            array(
                'tax_page' => $page_id
            ), __METHOD__
        );
    }

    public function saveTree() {
	if (! isset ($this->tree)) return true;
	if (count($this->tree) == 0) return true;
	
	$dbw=wfGetDB( DB_MASTER );
	$dbw->begin();
	// need to lock it
	// remove them all, then rebuild them all
	$this->deleteTree($dbw, $this->tree[0]->tax_page);
	foreach ($this->tree as $item) {
	    $item->saveItem($dbw);
	}
	$dbw->commit();
    }
    
    public static function removeTree($article) {
	$dbw = wfGetDB( DB_MASTER );
	$dbw->begin();
	TaxonomyTree::deleteTree($dbw, $article->getId());
	$dbw->commit();
    }

}


class Taxonomy {

    static function getRootId($dbr, $root) {
	$fields = array( 'page_id');
	$where = array( 'page_title' => $root );
	$row = $dbr->selectRow('page', $fields, $where, __METHOD__);
	return $row->page_id;
    }

    static function getTaxonomy($dbr, $page_id, $category) {
	$sql = "SELECT tax_page, tax_category, tax_parent, tax_left, tax_right, cat_title as category " .
	    "FROM taxonomy INNER JOIN category " .
	    "ON (tax_category = cat_id) " .
	    "WHERE (cat_title = '$category') AND (tax_page=$page_id) " .
	    "LIMIT 1";
	$res = $dbr->query($sql);
	if ($dbr->numRows( $res ) != 0) {
	    $row = $dbr->fetchObject ( $res );
	}
	$dbr->freeResult($res);
	return $row;
    }

    static function getNewArticles($dbr, $tax, $namespace, $count, $sort) {
	global $wgLang;
	$tax_left = $tax->tax_left;
	$tax_right = $tax->tax_right;
	$root_id = $tax->tax_page;
	$sql="SELECT page_namespace as namespace, page_title as title, page_touched as note " .
	    "FROM page " .
	    "INNER JOIN categorylinks ON page_id = cl_from " .
	    "INNER JOIN category ON cl_to = cat_title " .
	    "INNER JOIN taxonomy ON cat_id = tax_category " .
	    "WHERE (tax_page=$root_id) AND (tax_left>$tax_left) AND (tax_right<$tax_right) " .
	    "AND (page_namespace=$namespace) AND (page_is_redirect=0) " .
	    "ORDER BY page_touched $sort " .
	    "LIMIT $count";
	//print($sql);
	$res = $dbr->query($sql);
	while ( ( $row = $dbr->fetchObject($res)) ) {
	    $row->note=$wgLang->date($row->note, true);
	    $list[] = $row;
	    //print_r($row);
	}
	$dbr->freeResult($res);
	return $list;
    }

    static function getHotArticles($dbr, $tax, $namespace, $count, $sort) {
	$tax_left = $tax->tax_left;
	$tax_right = $tax->tax_right;
	$root_id = $tax->tax_page;
	$sql="SELECT page_namespace as namespace, page_title as title, page_counter as note " .
	    "FROM page " .
	    "INNER JOIN categorylinks ON page_id = cl_from " .
	    "INNER JOIN category ON cl_to = cat_title " .
	    "INNER JOIN taxonomy ON cat_id = tax_category " .
	    "WHERE (tax_page=$root_id) AND (tax_left>$tax_left) AND (tax_right<$tax_right) " .
	    "AND (page_namespace=$namespace) AND (page_is_redirect=0) " .
	    "ORDER BY page_counter $sort " .
	    "LIMIT $count";
	//print($sql);
	$res = $dbr->query($sql);
	while ( ( $row = $dbr->fetchObject($res)) ) {
	    $list[] = $row;
	    //print_r($row);
	}
	$dbr->freeResult($res);
	return $list;
    }

}

Taxonomy.i18n.php[edit]

<?php
/**
 * Internationalisation file for the Taxonomy extension
 *
 * @ingroup Extensions
 * @author Eko M. Budi
 */

$messages = array();

/** English
 * @author Eko M. Budi
 */
$messages['en'] = array(
	'taxonomy-tag'   => 'taxonomy',
	'taxonomy-tree'  => 'taxonomy-tree',
	'taxonomy-no-root' => 'missing root paramater in the taxonomy tag',
	'taxonomy-no-taxonomy'  => 'taxonomy is not exist',
	'taxonomy-no-category'  => 'category is not part of the taxonomy',
	'taxonomy-no-article'  => 'no article in this taxonomy',
);

/** English
 * @author Eko M. Budi
 */
$messages['id'] = array(
	'taxonomy-tag'   => 'taksonomi',
	'taxonomy-tree'  => 'pohon-taksonomi',
	'taxonomy-no-root' => 'parameter root tidak terdefinisi',
	'taxonomy-no-taxonomy'  => 'taksonomi tidak ada',
	'taxonomy-no-category'  => 'kategori tidak ada',
	'taxonomy-no-article'  => 'tidak ada artikel',
);


See also[edit]

This extension was inspired by :

Acknowledgments[edit]

This program took some code from Extension:Hierarchy by Fernando Correia and Extension:Dynamic_Article_List by Shannon McNaught.