Difference between revisions of "Extension:RecordAdmin"

From Organic Design wiki
(working for initial tests)
 
m
 
(37 intermediate revisions by 2 users not shown)
Line 1: Line 1:
<?php
+
{{legacy}}
# Extension:RecordAdmin{{Category:Extensions}}{{php}}{{Category:Extensions created with Template:SpecialPage}}
+
{{svn|RecordAdmin}}
# - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
 
# - Author: [http://www.organicdesign.co.nz/nad User:Nad]
 
  
if (!defined('MEDIAWIKI')) die('Not an entry point.');
+
The Record Administration extension forms the heart of our [[Wiki Organisation]] system.
  
define('RECORDADMIN_VERSION','0.0.1, 2007-10-17');
+
To export all the templates used by RecordAdmin, see [[wiki articles packages]]
  
$wgRecordAdminCategory = 'Records';
+
== RecordAdmin 1.0 ==
 +
For the last couple of years since [{{fullurl:{{FULLPAGENAME}}|oldid=87774}} RecordAdmin 0.0.1] was created in October 2007, we've kept the version numbering to 0.x to indicate it's experimental nature. Now halfway through 2010 we've been using it for our own project management and organisation and made a number of decisions about how version 1.0 should work. Here's a summary of our findings:
  
$wgExtensionFunctions[] = 'wfSetupRecordAdmin';
+
# Rather than use a category of templates to determine the record-types, 1.0 uses the titles in the NS_FORM namespace (i.e. all templates having an associated form are considered to be record-types). This is more logical since it's the form that determines the data types and field names within a record.
 +
# The record creation and editing should be done from the standard edit view rather than from a special page. This will be far simpler for the user since we could then remove the "properties" or "edit with form" action.
 +
# The special page is hardly ever used for searching since usually records are arrived at via a portal query. Since the special page would no longer be used for editing or creation either, it seems that it would be best for it to be removed from the main extension and made into a separate sub-extension like ''RecordAdminIntegratePerson'' and ''RecordAdminCreateForm''.
 +
# There should be allowed to be any number of records in a page as long as they are all of different types. And in fact as far as edit view goes, it shouldn't even have a problem with multiple of the same type. The records should all be at the beginning of the article and be removed from the standard edit box. We need to ensure that the method we use for this doesn't interfere with FCK or MCE.
 +
# We decided that for organisational purposes, GUID's are the best approach for naming, but I think it's still important to be able to add record types to any title, so we still need to handle the redirect issue.
 +
# Exporting needs to be handled as an API method, also allowing the standard lists such as JSON for AJAX queries
 +
# RA is very query-heavy, but a lot of the time it would be easily possible for RA to cache results and know when revisions occur which invalidate the cache - the very least of simply invalidating them all when any record changes would still offer a massive reduction in processing.
 +
# Record-table content could be allowed to load via AJAX after page load if not cached
  
$wgExtensionCredits['specialpage'][] = array(
+
=== Editing ===
'name'        => 'Special:RecordAdmin',
+
The main edit area and each record type form are hidden and expandable one at a time, the revealed one is settable in query-string. Record types can be added or removed dynamically and show their forms above the main textarea, the record wikitext is removed from the textarea and added on-save. The query-string can also include default values to use in the event that the record doesn't exist and so is being created. The parameters to do this are to be called ''preload::TYPE::FIELD'' since they operate in a similar way.
'author'      => '[http://www.organicdesign.co.nz/nad User:Nad]',
 
'description' => 'A special page for finding and editing record articles using a form',
 
'url'         => 'http://www.organicdesign.co.nz/Extension:SpecialExample',
 
'version'    => RECORDADMIN_VERSION
 
);
 
  
require_once "$IP/includes/SpecialPage.php";
+
We need to hook into the MW edit view and render record forms if the content contains any record template calls. Also a create record drop down needs to exist which can AJAXly add new records in to any article. All the existing record template calls must be removed from the wikitext textarea. The [[MW:Manual:Hooks/EditPage::showEditForm:initial]] is probably the best hook for adding record forms to the edit view.
  
# Define a new class based on the SpecialPage class
+
When an article is saved, any posted record forms must be converted into wikitext and prepended to the article text before the save occurs. The hook to use for this is [[MW:Manual:Hooks/ArticleSave]].
class SpecialRecordAdmin extends SpecialPage {
 
  
# Constructor
+
=== Caching ===
function __construct() {
+
caching uses either current cache layer or bag-of-stuff to store query result list and rendered queries by key. Keys include all the record type, filter parameters and user. Rendered results also include non-filter parameters such as template. Cache directives can be included in the queries to allow additional environment attributes to add to the key, or to mark as uncacheable. Invalidation occurs on-save and applies to all queries involving the same record-type, so recordtype must not be hashed.
SpecialPage::SpecialPage(
 
'RecordAdmin', # name as seen in links etc
 
'sysop',       # user rights required
 
true,          # listed in special:specialpages
 
false,        # function called by execute() - defaults to wfSpecial{$name}
 
false,         # file included by execute() - defaults to Special{$name}.php, only used if no function
 
false          # includable
 
);
 
}
 
  
# Override SpecialPage::execute()
+
=== API ===
# - $param is from the URL, eg Special:RecordAdmin/param
+
*query: returns either a standard API list of results, or rendered HTML
function execute($param) {
+
*create: the new "public form" method allowing records to be created without any login, edit form or saving required so bots and AJAX can do creates easily
global $wgOut, $wgRequest, $wgRecordAdminCategory;
 
$this->setHeaders();
 
$type = $wgRequest->getText('wpType') or $type = $param;
 
$record = $wgRequest->getText('wpRecord');
 
$title = Title::makeTitle(NS_SPECIAL, 'RecordAdmin');
 
$wpTitle = $wgRequest->getText('wpTitle');
 
  
# Get posted form values if any
+
=== AJAX ===
$posted = array();
+
*Record table queries could render non-cached content as an AJAX request so that the page can always load fast even when it contains non-cached data.
foreach ($_POST as $k => $v) if (ereg('^ra_(.+)$', $k, $m)) $posted[$m[1]] = $v;
+
*Maybe even revamp the livelets extension
  
# Read in the form for this record type if one has been selected
+
== Rendering performance improved ==
# - extract only the content from between the form tags and remove the submit input
+
One of our clients has some complex record tables which became extremely slow when there were more than 100 rows of data in the resultant set. After analysing the execution of the slow queries it turned out that over 90% of the processing time was in the ''renderRecords'' method. The problem was that the initialisation of the wiki parser takes a lot of resource, and it was being initialised once for every cell of data in the table. The problem was fixed (see [http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/RecordAdmin/RecordAdmin_body.php?r1=84686&r2=84839 diff]) by breaking the render loop out into two passes, the first to build a single block of wikitext for the whole table with delimiters for columns and rows, and the second to render the table drawing from the pre-parsed cell data rather than calling the wiki parser directly.
if ($type) {
+
[[Category:Legacy Extensions|RecordAdmin]]
$form = new Article(Title::newFromText("Form:$type"));
 
$form = $form->getContent();
 
$form = preg_replace('#<input.+?type=[\'"]?submit["\']?.+?/(input| *)>#', '', $form);
 
$form = preg_replace('#^.+?<form.+?>#s', '', $form);
 
$form = preg_replace('#</form>.+?$#s', '', $form);
 
$form = preg_replace('#name=[\'"](.+?)["\']#s', 'name="ra_$1"', $form);
 
}
 
 
 
# If no type selected, render select list of record types from Category:Records
 
if (empty($type)) {
 
$wgOut->addWikiText("== Select the type of record to search for ==\n");
 
 
 
$options = '';
 
$dbr  = &wfGetDB(DB_SLAVE);
 
$cl  = $dbr->tableName('categorylinks');
 
$cat  = $dbr->addQuotes($wgRecordAdminCategory);
 
$res  = $dbr->select($cl, 'cl_from', "cl_to = $cat", __METHOD__, array('ORDER BY' => 'cl_sortkey'));
 
while ($row = $dbr->fetchRow($res)) $options .= '<option>'.Title::newFromID($row[0])->getText().'</option>';
 
 
 
$wgOut->addHTML(wfElement('form', array('action' => $title->getLocalURL('action=submit'), 'method' => 'post'), null));
 
$wgOut->addHTML("<select name='wpType'>$options</select> ");
 
$wgOut->addHTML(wfElement('input', array('type' => 'submit', 'value' => 'Submit')).'</form>');
 
}
 
 
# Record type known, but no record selected, render form for searching
 
elseif (empty($record)) {
 
 
# Populate the form with the posted values
 
foreach ($posted as $k => $v) $form = str_replace("name=\"ra_$k\"", "name=\"ra_$k\" value='$v'", $form);
 
 
# Render the form
 
$wgOut->addWikiText("== Search for a $type record ==\n");
 
$wgOut->addHTML(wfElement('form', array('action' => $title->getLocalURL('action=submit'), 'method' => 'post'), null));
 
$wgOut->addHTML(
 
'<b>Record Name:</b> '
 
. wfElement('input', array('name' => 'wpTitle', 'size' => 30, 'value' => $wpTitle))
 
);
 
$wgOut->addHTML("\n<br><br><hr><br>\n");
 
$wgOut->addHTML($form);
 
$wgOut->addHTML(wfElement('input', array('type' => 'hidden', 'name' => 'wpType', 'value' => $type)));
 
$wgOut->addHTML('<br><hr><br>');
 
$wgOut->addHTML(wfElement('input', array('type' => 'submit', 'value' => 'Search')).'</form>');
 
 
# Render results if form has been posted
 
if (count($posted)) {
 
 
# Select records which use the template and exhibit a matching title and other fields
 
$records = array();
 
$dbr  = &wfGetDB(DB_SLAVE);
 
$tl  = $dbr->tableName('templatelinks');
 
$ty  = $dbr->addQuotes($type);
 
$res  = $dbr->select($tl, 'tl_from', "tl_namespace = 10 AND tl_title = $ty", __METHOD__);
 
while ($row = $dbr->fetchRow($res)) {
 
$t = Title::newFromID($row[0]);
 
$ttext = $t->getText();
 
if (empty($wpTitle) || eregi($wpTitle, $t)) {
 
$a = new Article($t);
 
$text = $a->getContent();
 
$match = true;
 
foreach ($posted as $k => $v) if ($v)
 
if (!(preg_match("|$k\s*=\s*(.+)\s*$|mi", $text, $m) && eregi($v, $m[1]))) $match = false;
 
if ($match) $records[] = $ttext;
 
}
 
}
 
 
 
# Print the list of results as links to edit records
 
$wgOut->addWikiText("<br>\n== $type records matching search query ==\n");
 
if (count($records)) {
 
$list = '';
 
foreach ($records as $r) {
 
$list .= "<li><a href='".$title->getLocalURL("wpType=$type&wpRecord=$r")."'>$r</a></li>\n";
 
}
 
$wgOut->addHTML("<ul>$list</ul>");
 
} else $wgOut->addWikiText("''no results''\n");
 
}
 
}
 
 
# A specific record has been selected, render form for updating
 
else {
 
$wgOut->addWikiText("== Editing \"$record\" ==\n");
 
$article = new Article(Title::newFromText("$type:$record"));
 
$text = $article->fetchContent();
 
 
 
# Update article if form posted
 
if (count($posted)) {
 
 
# Get the location and length of the record braces to replace
 
foreach ($this->examineBraces($text) as $brace) if ($brace['NAME'] == $type) $braces = $brace;
 
 
 
# Attempt to save the article
 
$summary = "[[Special:RecordAdmin|RecordAdmin]]: $type properties updated";
 
$replace = '';
 
foreach ($posted as $k => $v) if ($v) $replace .= "| $k = $v\n";
 
$replace = $replace ? "{{"."$type\n$replace}}" : "{{"."$type}}";
 
$text = substr_replace($text, $replace, $braces['OFFSET'], $braces['LENGTH']);
 
$success = $article->doEdit($text, $summary, EDIT_UPDATE);
 
 
# Report success or error
 
if ($success) $wgOut->addHTML("<div class='successbox'>$type updated successfully</div>\n");
 
else $wgOut->addHTML("<div class='errorbox'>An error occurred during update!</div>\n");
 
$wgOut->addHTML("<br><br><br><br>\n");
 
}
 
 
# Populate the form with the current values in the article
 
preg_match_all("|\|\s*(.+?)\s*=\s*(.+?)$|m", $text, $m);
 
foreach ($m[1] as $i => $k) $form = eregi_replace("name=\"ra_$k\"", "name=\"ra_$k\" value='{$m[2][$i]}'", $form);
 
 
# Render the form
 
$wgOut->addHTML(wfElement('form', array('action' => $title->getLocalURL('action=submit'), 'method' => 'post'), null));
 
$wgOut->addHTML($form);
 
$wgOut->addHTML(wfElement('input', array('type' => 'hidden', 'name' => 'wpType', 'value' => $type)));
 
$wgOut->addHTML(wfElement('input', array('type' => 'hidden', 'name' => 'wpRecord', 'value' => $record)));
 
$wgOut->addHTML('<br><hr><br>');
 
$wgOut->addHTML(wfElement('input', array('type' => 'submit', 'value' => 'Save')).'</form>');
 
}
 
 
}
 
 
 
function examineBraces(&$content) {
 
$braces = array();
 
$depths = array();
 
$depth = 1;
 
$index = 0;
 
while (preg_match('/\\{\\{\\s*([#a-z0-9_]*)|\\}\\}/is', $content, $match, PREG_OFFSET_CAPTURE, $index)) {
 
$index = $match[0][1]+2;
 
if ($match[0][0] == '}}') {
 
$brace =& $braces[$depths[$depth-1]];
 
$brace['LENGTH'] = $match[0][1]-$brace['OFFSET']+2;
 
$brace['DEPTH']  = $depth--;
 
}
 
else {
 
$depths[$depth++] = count($braces);
 
$braces[] = array(
 
'NAME'  => $match[1][0],
 
'OFFSET' => $match[0][1]
 
);
 
}
 
}
 
return $braces;
 
}
 
 
 
}
 
 
 
# Called from $wgExtensionFunctions array when initialising extensions
 
function wfSetupRecordAdmin() {
 
global $wgLanguageCode, $wgMessageCache;
 
 
 
# Add the messages used by the specialpage
 
if ($wgLanguageCode == 'en') {
 
$wgMessageCache->addMessages(array(
 
'recordadmin' => 'Record administration'
 
));
 
}
 
 
 
# Add the specialpage to the environment
 
SpecialPage::addPage(new SpecialRecordAdmin());
 
}
 

Latest revision as of 15:15, 6 July 2018

Legacy.svg Legacy: This article describes a concept that has been superseded in the course of ongoing development on the Organic Design wiki. Please do not develop this any further or base work on this concept, now this page is for historic record only.


Info.svg This code is in our Git repository here.

Note: If there is no information in this page about this code and it's a MediaWiki extension, there may be something at mediawiki.org.

The Record Administration extension forms the heart of our Wiki Organisation system.

To export all the templates used by RecordAdmin, see wiki articles packages

RecordAdmin 1.0

For the last couple of years since RecordAdmin 0.0.1 was created in October 2007, we've kept the version numbering to 0.x to indicate it's experimental nature. Now halfway through 2010 we've been using it for our own project management and organisation and made a number of decisions about how version 1.0 should work. Here's a summary of our findings:

  1. Rather than use a category of templates to determine the record-types, 1.0 uses the titles in the NS_FORM namespace (i.e. all templates having an associated form are considered to be record-types). This is more logical since it's the form that determines the data types and field names within a record.
  2. The record creation and editing should be done from the standard edit view rather than from a special page. This will be far simpler for the user since we could then remove the "properties" or "edit with form" action.
  3. The special page is hardly ever used for searching since usually records are arrived at via a portal query. Since the special page would no longer be used for editing or creation either, it seems that it would be best for it to be removed from the main extension and made into a separate sub-extension like RecordAdminIntegratePerson and RecordAdminCreateForm.
  4. There should be allowed to be any number of records in a page as long as they are all of different types. And in fact as far as edit view goes, it shouldn't even have a problem with multiple of the same type. The records should all be at the beginning of the article and be removed from the standard edit box. We need to ensure that the method we use for this doesn't interfere with FCK or MCE.
  5. We decided that for organisational purposes, GUID's are the best approach for naming, but I think it's still important to be able to add record types to any title, so we still need to handle the redirect issue.
  6. Exporting needs to be handled as an API method, also allowing the standard lists such as JSON for AJAX queries
  7. RA is very query-heavy, but a lot of the time it would be easily possible for RA to cache results and know when revisions occur which invalidate the cache - the very least of simply invalidating them all when any record changes would still offer a massive reduction in processing.
  8. Record-table content could be allowed to load via AJAX after page load if not cached

Editing

The main edit area and each record type form are hidden and expandable one at a time, the revealed one is settable in query-string. Record types can be added or removed dynamically and show their forms above the main textarea, the record wikitext is removed from the textarea and added on-save. The query-string can also include default values to use in the event that the record doesn't exist and so is being created. The parameters to do this are to be called preload::TYPE::FIELD since they operate in a similar way.

We need to hook into the MW edit view and render record forms if the content contains any record template calls. Also a create record drop down needs to exist which can AJAXly add new records in to any article. All the existing record template calls must be removed from the wikitext textarea. The showEditForm:initial is probably the best hook for adding record forms to the edit view.

When an article is saved, any posted record forms must be converted into wikitext and prepended to the article text before the save occurs. The hook to use for this is MW:Manual:Hooks/ArticleSave.

Caching

caching uses either current cache layer or bag-of-stuff to store query result list and rendered queries by key. Keys include all the record type, filter parameters and user. Rendered results also include non-filter parameters such as template. Cache directives can be included in the queries to allow additional environment attributes to add to the key, or to mark as uncacheable. Invalidation occurs on-save and applies to all queries involving the same record-type, so recordtype must not be hashed.

API

  • query: returns either a standard API list of results, or rendered HTML
  • create: the new "public form" method allowing records to be created without any login, edit form or saving required so bots and AJAX can do creates easily

AJAX

  • Record table queries could render non-cached content as an AJAX request so that the page can always load fast even when it contains non-cached data.
  • Maybe even revamp the livelets extension

Rendering performance improved

One of our clients has some complex record tables which became extremely slow when there were more than 100 rows of data in the resultant set. After analysing the execution of the slow queries it turned out that over 90% of the processing time was in the renderRecords method. The problem was that the initialisation of the wiki parser takes a lot of resource, and it was being initialised once for every cell of data in the table. The problem was fixed (see diff) by breaking the render loop out into two passes, the first to build a single block of wikitext for the whole table with delimiters for columns and rows, and the second to render the table drawing from the pre-parsed cell data rather than calling the wiki parser directly.