<?php if (!defined('PmWiki')) exit(); /* Copyright 2004 Patrick R. Michaud (pmichaud@pobox.com) This file is part of PITS (PmWiki Issue Tracking System), 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 file defines code needed for the PmWiki Issue Tracking System (PITS). The code is highly specialized towards the needs of pmwiki.org, and may not be useful (or even usable) anywhere else, but enough people have asked for it that I'm making it available in the Cookbook and wishing people the best of luck with it. Eventually there will likely be a "supported" version of this capability, and the supported version *may* even use the code below :-). But until Pm clears a few other items off of his to-do list, he may have limited ability to provide detailed support. (Offers of financial support have been known to significantly affect his development/support priorities, however. :) Issues with PITS can, of course, be registered in PITS on pmwiki.org (http://www.pmwiki.org/PITS). Okay, enough disclaimers, here's the code already. - ~Pm ---- A little bit later, Didier Lebrun, who was looking for this sort of application for a project, evaluated that PITS code could be quite easily de-specialized by moving the hardcoded stuff specializing it into configuration vars. So he did. The configuration is done mainly through template vars, with original PITS config being the default. Pm's original PITS script has only been changed when necessary, for the single purpose of making things configurable. The templates are as raw as possible, so as to be able to configure virtually anything while introducing as little overhead as possible. The de-specialized version should be able to run PITS as well as any other application of the same kind. - ~DidierLebrun ---- Usage: * Copy pits-*.php into cookbook/pits.php * Create a local/MyPitsGroup.php file * Put in it the vars you want to ajust (see Config vars below) * At the end of it, put: <?include_once("cookbook/pits.php"); * Create a MyPitsGroup.MyPitsForm page and put in it: (:pitsform:) * Create a MyPitsGroup.MyPitsList page and put in it: (:pitslist[ somefield=somevalue,othervalue otherfield=!excludevalue ...]:) where "somefield" is the name of a field and "somevalue,othervalue" are the values you want to list. On the opposite "otherfield=!excludevalue" will exclude the records where "otherfield" correspond to "excludevalue". But you can just put (:pitslist:) to display the full list. * ... That's it ! ---- Notes: * All vars in the templates MUST be escaped (ex: \$SomeVar) * Vars can be "local" ones or "global" ones * Local vars correspond to field names: (ex: $summary = content of the 'summary' field) * Global vars are those given by PmWiki, like $Author and $Now. Currently, $Author and $Now are the only ones supported, but you can add others in function PitsEvalTemplate(). You must avoid $...Url vars like $PageUrl and such, since the http://... link in them would be interpretated twice. But you can use [[...]] type of links anyway. * You can even use PHP functions in templates: ex: ".strftime('%Y-%m-%d %H:%M',$Now)." (see $PitsFormTemplateFmt) ---- History: 0.10 - Initial version released by Pm with the disclaimers above 0.20 - Modifications by Didier Lebrun <dl@vaour.net>: * De-specialized the original code (hardcoded -> config vars) * Stripped $PageUrl vars in template * Allowed space after label ("Label : content") * Limited parsing to the PITS header (leave out the comments below) * Allowed links in any table fields (ex: [[~Author]]) * Stripped LF and other spaces between HTML lines (avoid vspace) * Added HTML entities charset for some systems (mainly NTFS ?) 0.21 - Modifications by Didier Lebrun <dl@vaour.net>: * Fixed label parsing for i18n charsets * Fixed description entry in edit template (LF problem) * Transfered default values from edit template to form template * Suppressed unuseful $PitsGroup global var * Added (:pitstrail[ PageName|Label]:) markup and assorted function 0.22 - Modification by Fabrice Mousset <fabrice.mousset@laposte.net> * Fixed (:pitlist:) makeup (directive ==> directives) * Fixed misspelled variable $PitsIDLenght ==> $PitsIDLength * Fixed PITS page list to add $PitsIDLength into scan pattern * Fixed FmtPitsList() function when no PITS are defined (deadlock) * Fixed PitsTrail() function when less than 2 PITS are defined (deadlock) 0.23 - Modification by Fabrice Mousset <fabrice.mousset@laposte.net> * Fixed html code output to remove '\' miss placed * Added $PitsNoItemText variable to personalize output when no PITS present */ define(PITS_VERSION, '0.23'); # ---- Config vars ---- # ## These vars can be overriden in the PITS group configuration file ## (local/YourPITSGroupName.php). Whenever you see something like: ## SDV($SomeVar, "default setup"); ## you must put in your config something like: ## $SomeVar = "custom setup"; ## SDV() is a just PmWiki function allowing to set a default value ## when none has been setup in the config. ## Number of chars for page names SDV($PitsIDLength, 5); ## Template for empty PITS list SDV($PitsNoItemText, "No bugs ;-)"); ## Label to field name mapping. Labels and names can be whatever you like, ## as long as they are consistent through all configuration vars. SDV($PitsLabels, array( 'Summary' => 'summary', 'Created' => 'created', 'Status' => 'status', 'Category' => 'category', 'From' => 'author', 'Assigned' => 'assigned', 'Priority' => 'priority', 'Version' => 'version', 'OS' => 'os', 'Description' => 'description' )); ## Template for the input form SDV($PitsFormTemplateFmt, " <form method='post'> <input type='hidden' name='action' value='postpits' /> <input type='hidden' name='created' value='" . strftime('%Y-%m-%d %H:%M', $Now) . "' /> <input type='hidden' name='status' value='Open' /> <table class='pitsform'> <tr> <td class='label'>Author:</td> <td class='input'><input type='text' name='author' value='\$Author' /></td> </tr> <tr> <td class='label'>Summary:</td> <td class='input'><input type='text' name='summary' size='60' /></td> </tr> <tr> <td class='label'>Category:</td> <td class='input'> <select name='category'> <option value=''></option> <option value='Bug'>Bug</option> <option value='Feature'>Feature/Change Request</option> <option value='Documentation'>Documentation</option> <option value='Cookbook'>Cookbook</option> <option value='PHP Compatibility'>PHP Compatibility</option> <option value='Other'>Other</option> </select> </td> </tr> <tr> <td class='label'>Priority:</td> <td class='input'> Low <input type='radio' name='priority' value='1' /> <input type='radio' name='priority' value='2' /> <input type='radio' name='priority' value='3' /> <input type='radio' name='priority' value='4' /> <input type='radio' name='priority' value='5' /> High </td> </tr> <tr> <td class='label'>PmWiki Version:</td> <td class='input'><input type='text' name='version' /></td> </tr> <tr> <td class='label'>OS/Webserver/<br />PHP Version:</td> <td class='input'><input type='text' name='os' /></td> </tr> <tr> <td class='label' valign='top'>Description:</td> <td class='input'><textarea name='description' cols='60' rows='15'></textarea></td> </tr> </table> <div align='center'><input type='submit' value='Submit new issue' accesskey='s' /></div> </form> "); ## Template for filling the edit box with input data ## "----" MUST be present at the end to separate the entries from the ## eventual comments that could be added afterwards. SDV($PitsEditTemplateFmt, " Summary: \$summary Created: \$created Status: \$status Category: \$category From: [[~\$author]] Assigned: \$assigned Priority: \$priority Version: \$version ##OS: os (changed 2013/05/04) to OS: \$os OS: \$os Description:[[<<]]\$description ---- !!!Comments ---- (:pitstrail:) "); ## Template for list table. ## The "(/)PitsListItemFmt" tags MUST be present as delimiters of the entry part. SDV($PitsListTemplateFmt, " <table border='1' cellspacing='0' class='pitslist'> <tr> <th><a href='?order=-name'>Issue#</a></th> <th><a href='?order=created'>Created</a></th> <th><a href='?order=category'>Category</a></th> <th><a href='?order=version'>Version</a></th> <th><a href='?order=-priority'>Priority</a></th> <th><a href='?order=status'>Status</a></th> <th><a href='?order=summary'>Summary</a></th> </tr> <!--PitsListItemFmt--> <tr> <td>[[\$name]]</td> <td>\$created</td> <td>\$category</td> <td>\$version</td> <td>\$priority</td> <td>\$status</td> <td>\$summary</td> </tr> <!--/PitsListItemFmt--> </table> "); ## HTML styles SDV($PitsStylesFmt, " .pitsform {} .pitsform td.label { text-align:right; font-weight:bold; } .pitsform td.input {} .pitspage {} .pitspage span.label {} .pitspage span.data {} .pitslist {} .pitslist th { background-color:#eeeeee; } .pitslist th a { text-decoration:none; } "); ## Edit form top message SDV($PitsEditMessageFmt, "<p class='vspace'>Please review and make any edits to your issue below, then press 'Save'.</p>"); ## Fields that can be filtered by name=criteria args SDV($PitsListFilters, array('summary','created','status','category','priority','version')); ## Criteria used for default and complementary sorting after explicit criteria SDV($PitsListDefaultSort, array('-priority','status','category','name')); ## Charset for HTML entities conversion ('ISO-8859-1' usually, 'UTF-8' on NTFS systems) SDV($PitsHtmlEntitiesCharset, 'ISO-8859-1'); # ---- end of Config vars ---- # $HTMLStylesFmt[] = $PitsStylesFmt; markup('pitsform', 'inline', '/\\(:pitsform:\\)/e', "Keep(PitsForm(\$pagename))"); markup('pitslist', 'directives', '/\\(:pitslist\\s*(.*?):\\)/e', "FmtPitsList('', \$pagename, array('q' => PSS('$1')))"); markup('pitsread', 'directives', '/^('.implode('|', array_keys($PitsLabels)) . ')\s*:(.*)/', "<:block><div class='pitspage'><span class='label'>$1:</span> <span class='data'>$2</span></div>"); markup('pitstrail', '<links', '/\\(:pitstrail\\s*(.*?):\\)/e', "PitsTrail(\$pagename,'$1')"); include_once("$FarmD/scripts/author.php"); ## PitsForm() generates the form for entering a new issue. Note that ## once an issue has been created, it's a normal wikipage and is edited ## according to the normal editing code (i.e., there's no form-based ## editing yet). function PitsForm($pagename) { global $PitsFormTemplateFmt; $tmpl = preg_replace('/\s*\n\s*/', '', $PitsFormTemplateFmt); $out = PitsEvalTemplate(array(), $tmpl); return FmtPageName($out, $pagename); } ## Fills the edit form with formatted input if ($action == 'postpits') { list($group) = explode('.', $pagename); Lock(2); # $pitslist = ListPages("/^$group\\.\\d/"); $pitslist = ListPages("/^$group\\.\\d{" . $PitsIDLength . "}$/"); $issue = 0; foreach($pitslist as $i) $issue = max(@$issue, substr($i, strlen($group) + 1)); $pagename = sprintf("$group.%0" . $PitsIDLength . "d", @$issue + 1); $action = 'edit'; $EditMessageFmt = $PitsEditMessageFmt; $_REQUEST['post'] = 1; $_POST['text'] = PitsEvalTemplate($_REQUEST, $PitsEditTemplateFmt); } ## FmtPitsList() creates a table of PITS issues according to various ## criteria. function FmtPitsList($fmt, $pagename, $opt) { global $PitsListTemplateFmt, $PitsLabels, $PitsIDLength, $PitsHtmlEntitiesCharset, $PitsListFilters, $PitsListDefaultSort, $PitsNoItemText; list($group) = explode('.',$pagename); $opt = array_merge($opt, @$_REQUEST); $pitslist = ListPages("/^$group\\.\\d{" . $PitsIDLength . "}$/"); ## If not PIT available, display default contents if(count($pitslist) < 1) { return FmtPageName("$PitsNoItemText", $pagename); } $tmpl = preg_replace('/\s*\n\s*/', '', $PitsListTemplateFmt); $sect = preg_split('#[[<]!--(/?(?:PitsListItemFmt)\\s?.*?)--[]>]#', $tmpl); if(count($sect) != 3) Abort("\$PitsListTemplateFmt MUST have 3 sections !"); $out[] = FmtPageName(array_shift($sect), $pagename); $terms = preg_split('/((?<!\\S)[-+]?[\'"].*?[\'"](?!\\S)|\\S+)/', $opt['q'], -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); foreach($terms as $t) { if (trim($t) == '') continue; if (preg_match('/([^\'":=]*)[:=]([\'"]?)(.*?)\\2$/', $t,$match)) $opt[strtolower($match[1])] = $match[3]; } $n = 0; $plist = array(); foreach($pitslist as $p) { $page = ReadPage($p); list($head) = preg_split('/\n----/', $page['text'], 0); preg_match_all('/(^|\n)([^:]*):(\s*\\[\\[<<\\]\\])?([^\n]*)/', $head,$match); $fields = array(); for($i = 0; $i < count($match[2]); $i++) $fields[$PitsLabels[trim($match[2][$i])]] = htmlentities(trim($match[4][$i]), ENT_QUOTES, $PitsHtmlEntitiesCharset); foreach($PitsListFilters as $h) { if (!@$opt[$h]) continue; foreach(preg_split('/[ ,]/', $opt[$h]) as $t) { if (substr($t, 0, 1) != '-' && substr($t, 0, 1) != '!') { if (strpos(strtolower(@$fields[$h]), strtolower($t)) === false) continue 3; } else if (strpos(strtolower(@$fields[$h]), strtolower(substr($t,1))) !== false) continue 3; } } $plist[$n] = $fields; list(, $plist[$n]['name']) = explode('.', $p); $n++; } if($n == 0) { return FmtPageName($PitsNoItemText, $pagename); } $cmp = CreateOrderFunction(implode(',', array_merge(array(@$opt['order']), (array)$PitsListDefaultSort))); usort($plist, $cmp); $tr = array_shift($sect); foreach($plist as $p) { $out[] = PitsEvalTemplate($p,$tr); } $out[] = array_shift($sect); return FmtPageName(implode('', $out), $pagename); } ## This function creates a WikiTrail based on the current page number function PitsTrail($pagename, $trailindex='') { global $PitsIDLength; list($group, $name) = explode('.', $pagename); trim($trailindex); if(empty($trailindex)) $trailindex = $group; $trailindex = '[[' . $trailindex . ']]'; # $pitslist = ListPages("/^$group\\.\\d+$/"); $pitslist = ListPages("/^$group\\.\\d{" . $PitsIDLength . "}$/"); ## There must be at least 2 PITS to display the trail! if(count($pitslist) < 2) return ""; sort($pitslist); $i = @$name-1; list(,$min) = explode('.', array_shift($pitslist)); for($i; $i >= @$min; $i--) { $prev = sprintf("%0" . $PitsIDLength . "d", @$i); if(PageExists("$group.$prev")) { $prev = '[[' . $prev . ']]'; break; } $prev = ''; } $i = @$name+1; list(,$max) = explode('.', array_pop($pitslist)); for($i; $i <= @$max; $i++) { $next = sprintf("%0" . $PitsIDLength . "d", @$i); if(PageExists("$group.$next")) { $next = '[[' . $next . ']]'; break; } $next = ''; } return "<span class='wikitrail'><< $prev | $trailindex | $next >></span>"; } ## This function creates specialized ordering functions needed to ## (more efficiently) perform sorts on arbitrary sets of criteria. function CreateOrderFunction($order) { $code = ''; foreach(preg_split('/[\\s,|]+/', strtolower($order), -1, PREG_SPLIT_NO_EMPTY) as $o) { if (substr($o, 0, 1) == '-') { $r = '-'; $o = substr($o, 1); } else $r=''; if (preg_match('/\\W/', $o)) continue; $code .= "\$c=strcasecmp(@\$x['$o'],@\$y['$o']); if (\$c!=0) return $r\$c;\n"; } $code .= "return 0;\n"; return create_function('$x,$y', $code); } ## This function is used to evaluate the templates while insulating their ## vars from potential name conflicts function PitsEvalTemplate($hash, $tmpl) { global $Author,$Now; foreach($hash as $k=>$v) $$k = $v; return(@eval("return(\"$tmpl\");")); } ?>