Manual:Tag extensions/Example

From MediaWiki.org
Jump to navigation Jump to search

Tag extension example - an interactive tag[edit]

In this tutorial you'll create a new interactive tag which creates a form to allow a user to give feedback on the page he/she is visiting:

+------------------------------------------------------------+
| Feedback:                   | [__________________________] | <= input text field
| Previous feedback given on: | (never)                      |
| Clear history:              | [ ]                          | <= check box
+------------------------------------------------------------+
|                         [ SUBMIT ]                         | <= button
+------------------------------------------------------------+

When the submit button is clicked, the values of the input fields are sent to the wiki and stored in the wiki database. The form shows when the last feedback was given, if any. (The tutorial is developed with MediaWiki 1.20.3, which incorporates jQuery 1.8.2. Ajax needs to be enabled, which it is by default.)

Define the tag[edit]

The first step is to define the tag. The examples on Manual:Tag extensions are a good start. You can also look at how tag extensions have been made by others, but most extensions are quite complicated to analyze.

At the very minimum, you need a PHP-script which generates the HTML-code for the form, and you need to instruct your wiki to run this script whenever the tag is found on a page. We'll take a slightly more involved approach. We'll include the script in a class definition, and create two separate PHP-script files, just as MediaWiki says we should.

Let's call the tag 'poll', so you are going to type <poll> on your page to include the form above.

The scripts we are making, will reside in a subfolder of the extensions-folder of your wiki. Create a subfolder 'poll', and create a text file 'poll.php' there. Make sure the wiki process is allowed to run these scripts.

Copy the following few lines into the 'poll.php' script:

<?php
$wgAutoloadClasses['Poll'] = $IP . '/extensions/Poll/poll_body.php';
$wgHooks['ParserFirstCallInit'][] = 'Poll::onParserInit';

The first line instucts the wiki where to find class 'Poll'. $IP is the install path. The array $wgHooks contains all functions your wiki must call if specific situations occur. We are adding a function for when a parser gets initialized. A parser reads the page definition and creates the HTML-code for the actual page, and must know from the beginning that there may be a poll-tag which it should process. The function called is the onParserInit-function of class Poll. This function is defined in the second file, 'poll_body.php'. Create it and add these lines:

<?php
class Poll {
	static function onParserInit( Parser $parser ) {
		$parser->setHook( 'poll', array( __CLASS__, 'pollRender' ) ); 
		return true;
	}
	static function pollRender( $input, array $args, Parser $parser, PPFrame $frame ) {
		$ret = '<table>';
		$ret .= '<tr>';
		$ret .= '<td>Feedback</td>';
		$ret .= '<td><input id="inp001" type="text" /></td>';
		$ret .= '</tr>';
		$ret .= '<tr>';
		$ret .= '<td>Previous feedback given on:</td>';
		$ret .= '<td>(never)</td>';
		$ret .= '</tr>';
		$ret .= '<tr>';
		$ret .= '<td>Clear history</td>';
		$ret .= '<td><input id="chk001" type="checkbox" /></td>';
		$ret .= '</tr>';
		$ret .= '</table>';
		$ret .= '<input type="submit"/>';
		return $ret;
	}
}

The Poll class defines two functions: onParserInit() and pollRender(). The first one is called when the page parser initializes. It does just one thing: it tells the parser to call pollRender() when a poll-tag is encountered. The variable __CLASS__ means 'this class'; we could have written 'Poll' here. The function pollRender() creates the HTML-code of the form and returns it.

Before it works, there is one more thing to do. Edit LocalSettings.php (in the root folder of your wiki), and add at the end of the file:

# Extension Poll
require_once( "$IP/extensions/Poll/poll.php" );

Now pick a page from your wiki and put the tag <poll> </poll> somewhere on it. Save and see what shows up.

If you made a mistake, you may well find that however hard you try to correct it, you still seem to get the same error. That may be caused by the wiki cache, which does not automatically load the new script. You can clear the cache by reloading the page with an additional '&action=purge' appended to the page URL.

Include a custom stylesheet for the tag[edit]

The form does not stand out very well, so we are going to give it a border, using a stylesheet. Create a subfolder 'resources' in the Poll extension folder, and create a file 'poll.css' there. Add the following lines to it:

table.wtable {
	border-style:solid; border-width:1px; border-collapse:collapse;
}

To use this stylesheet, first update pollRender() to include the style class 'wtable':

$ret = '<table class="wtable">';

Then define the stylesheet and its location in poll.php by adding a module with the name 'ext.poll':

$wgResourceModules['ext.poll'] = array(
	'localBasePath' => __DIR__,
	'remoteExtPath' => 'Poll',
	'styles' => 'resources/poll.css'
);

Finally instruct the parser to load the stylesheet, by adding to onParserInit():

static function onParserInit( Parser $parser ) {
		global $wgOut;
		
		$parser->setHook( 'poll', array( __CLASS__, 'pollRender' ) ); 
		$wgOut->addModules( 'ext.poll' );
		return true;
	}

$wgOut is another interesting global object. Now retry the page. Don't forget to purge.

Making the button do something[edit]

The submit button doesn't do anything, as you may have noticed. Of course it should, but this gets complicated quite easily. So we start with something simple and straightforward as showing a messagebox with a text.

Until now the scripts were all PHP, but now we get into a domain where JavaScript is the common programming language. A MediaWiki wiki supports a JavaScript extension called jQuery, which simplifies the necessary scripts considerably, but it may be quite confusing if you have never seen it before. Check www.jquery.com for details.

We start by giving the button an id, so we can identify it. Modify pollRender() in 'poll_body.php':

$ret .= '<tr>';
$ret .= '<td align="center" colspan=2><input id="btn001" type="button" value="Submit"></td>';
$ret .= '</tr>';

Now we can write the JavaScript which opens a message box when the button is clicked. Put the script into a second file in the resources folder, called 'poll.js':

$("#btn001").click(function() {
	alert("Button clicked.");
});

The $-sign is a jQuery abbreviation, $("#btn001") selects the element with id 'btn001'. The rest of the script specifies what is to be done if the element is clicked. Since we want this script to be loaded, we add it to the module specification in poll.php:

$wgResourceModules['ext.poll'] = array(
	'scripts' => 'resources/poll.js',
	'localBasePath' => __DIR__,
	'remoteExtPath' => 'Poll',
	'styles' => 'resources/poll.css'
);

Time to retry the page. When you click the button, a message box should appears. If you make an error in the javascript, you may have to clear the browser cache, before you reload the page with the purge action.

Send the form data to the poll on a button click[edit]

Clicking the button should send all form data to the poll extension. We need to modify the JavaScript click handler, to send a so-called HTTP GET request to the wiki server, with the page URL as a basis and the form parameters concatenated to it. It is difficult to find clear information on this topic. Most examples suggest to use the jQuery function $.ajax() for this purpose, but I would prefer $.get() since it is much simpler. The click handler becomes:

$("#btn001").click(
	function() {
		var feedback = $("#inp001").val();
		var clear = $("#chk001").is(':checked');
		$.get(
			mw.util.wikiScript(),
			{
				action: 'ajax',
				rs: 'Poll::submitVote',
				rsargs: [feedback, clear]
			}
		);
	}
);

Clicking the button initializes two variables with the values of the textbox and checkbox, and activates $.get(), with two parameters. The first one specifies the base URL to use for the request. The second one is an array. It specifies (1) the kind of action, in this case an 'ajax' asynchronous request, (2) the function which will react on the request: the submitVote() function of the Poll class, and (3) the arguments to be sent to the submitVote() function. There are two arguments: the feedback text and the setting of the clear checkbox.

Define the submitVote() function in poll_body.php:

public static function submitVote( $feedback, $clear ) {
	wfErrorLog( "submitVote() called text=" . $feedback . " clear=" . $clear . "\n",
                    '/tmp/poll001.log' );
	return true;
}

The function has two arguments, corresponding to the parameters sent in the GET request. It echoes the parameter values into a custom log file, UNIX style here, which is not very useful but sufficient to see the effect.

One more thing needs to be done: the submitVote() function must be registered as a valid function for responding to a request. To register, add the following line to poll.php:

$wgAjaxExportList[] = 'Poll::submitVote';

Now clear the browser cache (to force a reload of all javascript) and reload the page with the purge action. Type some text in the feedback box and check the 'clear' checkbox. Click on 'submit', and check the log file. It should contain a line with the text you typed, followed by 'clear=true'.

If you want to see the GET request, add a line to LocalSettings.php (see Manual:How to debug):

$wgDebugLogFile = "/tmp/{$wgSitename}-debug_log.txt";

The log file will contain a line with the actual request. If your page is the wiki main page, the text is 'Simple poll response' and the checkbox is cleared, then the request becomes:

GET /mediawiki-1.20.3/index.php?action=ajax&rs=Poll%3A%3AsubmitVote&rsargs%5B%5D=Simple+poll+response&rsargs%5B%5D=false

If the checkbox is checked, we get:

GET /mediawiki-1.20.3/index.php?action=ajax&rs=Poll%3A%3AsubmitVote&rsargs%5B%5D=Simple+poll+response&rsargs%5B%5D=true

The %XX codes represent a colon (%3A) and square brackets (%5B and %5D)

Saving the poll input in the database[edit]

The last sections of this tutorial cover the interaction with the database. We want to store the poll response in the wiki database, to be able to do something with it later. First of all, we need to create a database table to store the poll results in. Depending on the database your wiki is using, you may have to execute a different set of commands for this, but they will always be similar to what I'll describe here. (For details: Manual:Database access.) I'm using a MySQL database, so the first step will be to open the database administration tool 'mysql'. You need your wiki's database name and the administrator's name and password. You can look it up if necessary, in LocalSettings.php ($wgDBname, $wgDBuser and $wgDBpassword). If the wiki database is called polldb and the user is polladmin, you start mysql by:

$ mysql -u polladmin -p polldb

Specify the password, and at the mysql prompt enter:

CREATE TABLE IF NOT EXISTS polldb.poll (
  `poll_key` MEDIUMINT PRIMARY KEY NOT NULL AUTO_INCREMENT,
  `poll_user` varchar(32) NOT NULL default '',
  `poll_feedback` varchar(32) NOT NULL default '',
  `poll_clear` smallint,
  `poll_date` datetime default NULL
) ENGINE=InnoDB, DEFAULT CHARSET=binary;

The appropriate table options are also specified in LocalSettings.php: $wgDBTableOptions. Check that the table is empty, by entering at the mysql prompt:

select * from poll;

which should return

Empty set (0.00 sec)

Keep mysql running, since we are going to check the table contents in a minute.

We need to modify onSubmitVote(). Replace the existing body with the follwing lines:

global $wgUser;
wfErrorLog( "submitVote() called text=" . $feedback . " clear=" . $clear . "\n", '/tmp/poll001.log' );
// Database
$dbw = wfGetDB( DB_MASTER );
// User
$user = $wgUser->getName();
// Insert vote
$insertQuery = $dbw->insert(
	'poll',
	array(
		'poll_user' => $user,
		'poll_feedback' => $feedback,
		'poll_clear' => ($clear == 'true' ? 1 : 0),
		'poll_date' => wfTimestampNow()
	)
);
$dbw->commit();
return true;

The function now opens the database (for writing, therefore $dbw), gets the name of the current user from $wgUser, inserts data into the database, and commits it. The data is inserted into table 'poll' (1st argument of the insert-function), and consists of four parts, for the table fields specified.

Let's try it out. Submit two text-strings, once with the clear checkbox checked and once cleared. Return to the mysql prompt and reissue the select statement. The result will be something like:

+----------+------------------+----------------+------------+---------------------+
| poll_key | poll_user        | poll_feedback  | poll_clear | poll_date           |
+----------+------------------+----------------+------------+---------------------+
|        1 | Janvle           | First feedback |          1 | 2013-04-12 21:01:25 |
|        2 | Janvle           | Next feedback! |          0 | 2013-04-12 21:01:45 |
+----------+------------------+----------------+------------+---------------------+
2 rows in set (0.00 sec)

Check the formatting of the database tables if there is an issue with entered data.

Clearing the poll history[edit]

If the clear checkbox is checked, all poll results for the current user should be removed, so the poll starts from scratch. Deleting the poll results is done in onSubmitVote(). Add a delete-branch, so the function body becomes:

global $wgUser;
wfErrorLog( "submitVote() called text=" . $feedback . " clear=" . $clear . "\n",
            '/tmp/poll001.log' );
// Database
$dbw = wfGetDB( DB_MASTER );
// User
$user = $wgUser->getName();
// Distinguish between clear true and false
if ($clear == 'true') {
	// Delete poll results
	$deleteQuery = $dbw->delete(
		'poll',
		array(
			'poll_user' => $user
		)
	);
	$dbw->commit();
} else {
	// Insert vote
	$insertQuery = $dbw->insert(
		'poll',
		array(
			'poll_user' => $user,
			'poll_feedback' => $feedback,
			'poll_clear' => ($clear == 'true' ? 1 : 0),
			'poll_date' => wfTimestampNow()
		)
	);
	$dbw->commit();
}
return true;

Try it out. Check the clear-checkbox, and reissue the select statement in mysql. The poll table will be empty again.

Getting feedback into the form[edit]

The last part of this tutorial will do two things related to informing the user of past poll results. In this example this is very simple. In the field next to 'Previous feedback given on:' we'll display the date of the previous poll input. First of all, this date must be read from the database when the tag is rendered. But the form must also be updated after you have clicked the submit button.

To get the date of the last poll submission from the database, modify pollRender(). Add these lines to the beginning of the function body:

global $wgUser;
// Database
$dbr = wfGetDB( DB_SLAVE );
// User
$user = $wgUser->getName();
// Get data
$rs = $dbr->select(
	'poll',
	'poll_date',
	array('poll_user' => $user),
	__METHOD__,
	array( 'ORDER BY' => 'poll_date DESC' )
);
if ( $rs->numRows() == 0 ) {
	$recentDate = '(unknown)';
} else {
	$row = $rs->fetchRow();
	$recentDate = $row['poll_date'];
}

and modify the line with '(never)' as well, to use the value of $recentDate:

$ret .= "<td>$recentDate</td>";

We need double quotes now; with single quotes no variable sustitution will take place.

Check this by submitting a couple of poll texts, and the reload the page with a purge action. It's not perfect, since the poll form will show GMT instead of local time, but you get the idea.

Now finally, the date should also update after a poll text submission. We achieve this by having submitVote() return the current time, and have the button click handler update the form asynchronously. The changes in submitVote() are (1) an additional line at the end of the delete-branch:

$ret = '(none)';

(2) a similar line at the end of the insert-branch:

$ret = wfTimestamp( TS_DB, time() );

and (3) let submitVote() return $ret:

return $ret;

Then add an identification to the cell which must be updated. Modify the line with the recent date in pollRender() so that this cell has an id-attribute:

$ret .= "<td id='dat001'>$recentDate</td>";

Finally modify the click handler in poll.js to act when the $get() function is complete. It becomes:

$("#btn001").click(
	function() {
		var feedback = $("#inp001").val();
		var clear = $("#chk001").is(':checked');
		$.get(
			mw.util.wikiScript(),
			{
				action: 'ajax',
				rs: 'Poll::submitVote',
				rsargs: [feedback, clear]
			}
		).done(
			function(data) {
				$("#dat001").html(data);
			}
		);
	}
);

The 'data' variable in the done-function contains the date returned by Poll::submitVote(). It replaces the current html content of the table cell, so now the most recent poll date is immediately reflected in the form, when you click the button. Remeber to clear the browser cache, if you don't see anything happen after clicking the button.

Summing it all up[edit]

We created a custom tag <poll> which displays a very simple feedback form on a wiki page. A reader can type in some text and submit the feedback, which is stored in the wiki database. The form shows the last time the reader submitted feedback, and can also instruct all poll information to be cleared.

In more general terms, we created a structured way for a wiki user to interact with a page, with information going from reader to wiki-database and back.

Of course this example is all very basic and not immediately useful, but if you need a tag extension with user interaction, I hope I got you started. Good luck with further improvements!