WikiForms Recipes

Summary: Various recipes for the WikiForms recipe
Version:
Prerequisites:
Status:
Maintainer:
Categories: Forms

Assign and reference titles for numeric page names

This is a 2 part feature: assign titles to pages with numeric names; and reference a numeric page by its title.

To assign a title, declare one of the fields to be type (title). As of version 1.42, WikiForms does 3 things:

  • it wraps the value of the title field in a (:title:) directive
  • it uses the value of the title field as the change summary, unless another field is designated as the summary (remember that you can do this by putting an * against the summary element in the form template)
  • if the title field is used in a wikilist, it makes this a link to the corresponding page number

Now we need a way to reference a page by its title. formtitle.phpΔ answers this requirement. This adds [[?page title]] markup to resolve references to pages that include the page's title in the RecentChanges change summary. You need to tell it the location of a page in your forms group where the new entry form is:

    $NewGlossaryPage = 'Glossary.NewEntry';    //for example
    include_once("$FarmD/cookbook/formtitle.php");

If you now write [[?some title]] anywhere in your wiki, the formtitle script does several things:

  • use `Glossary.RecentChanges to look up the title -- this is why the new entry page must be in the forms group and why the (title) is automatically put into the change summary
  • if no title is found, the markup links to `Glossary.NewEntry and passes the title as the default value for the field designated as the (title) -- this is why we have to tell it the location of the new entry form
  • if it finds exactly one title match, it links to it
  • if it finds more than one title match (titles, unlike page names, do not need to be unique), it links to each of them in the order found on `RecentChanges

Note that this markup assumes you only want to do this for one forms group per wiki. It needed a special and unambiguous markup and I chose [[?some title]] -- the price we pay, like for profiles and categories, is that it limits us to one title-based group. Using wikiforms without assigning a 5 digit name is problematic and would probably require a complete re-write of the recipe. Wikiforms knows to use a form because the page name is numeric.


Pagelist/searchbox template to search through Wikiform groups

Description

I'm using this template to search through multiple Wikiform groups from a searchbox directive. The results are displayed as a table with the following structure (links in example are fakes):

GroupPageTitle
Group n00042:title:Main Document
Group m00252:title:Supplementary Document

The title information is taken from paragraph 9 on my Wikiform pages. It would be cool if I could suppress the :title: markup that precedes the actual document title (documents are stored as attachments of the wikiform pages), but I don't know how to do that.

A slight problem is that pages in the searched groups that are not Wikiform pages are included in the list as well, leading to random lines to be included in the title field. Another slight problem is that the number of titles displayed is limited by $maxincludes, which might not always be enough. (I have set the value to 250, but there are ca. 3200 documents in my application.)

I could exclude the non-document pages as they do not start with a numeric character, but that would load up the searchbox with a rather lengthy string (searching pages 0*, 1*, 2*, 3*, ..., 9*), but I haven't implemented it that way yet because it seems crude. I'll look for a smarter way (if there is any). --Henning October 19, 2006, at 10:27 AM

Template

[[#headerinclude]]
(:if equal {<$Group}:)
(:table cellspacing:5px:)
(:cell:)'''Group'''
(:cell:)'''Page'''
(:cell:)'''Title'''
(:cell:)
(:if:)
(:cellnr:)[[{=$Group}]]
(:cell:)[[{=$FullName}|{=$Title}]]
(:cell:)(:include {=$FullName} lines=9..9:)
(:if equal {>$Group}:)
(:tableend:)
----
(:if:)
[[#headerincludeend]]

authorplain field type

This field type allows you to populate it with the author's name automatically, but without the profile markup.

In EntryForm(), insert the following clause (may need to appear before the one matching "author"):

        } elseif (preg_match('/^authorplain(?:=(\\d+))?$/',$f[$i]['etype'],$m)) {
            $col = ($m[2]) ? $m[2] : 32;
            if ($editing) $author = DeLink($fv[$f[$i]['element']]);
            else $author = FmtPageName('$Author',$pagename);
            $out[] = "<input type='text' size='$col' name='".$f[$i]['element'].
                    "' value='$author' />";

Also, in FormData(), insert the following clause:

        if (preg_match('/^authorplain/',$f[$i]['etype'])) {
            $v = $_REQUEST[$f[$i]['element']];
        }

shi December 20, 2006, at 02:44 PM

Added in version 1.0.52 jr March 22, 2007, at 06:38 PM

buttondate field type

I added this field type to allow the convenience of a button for setting today's date while editing an existing form, e.g. "date closed" of an issue tracking system. Note that the "Today" button is only shown while the date is undefined, to ensure that if the user changes an existing date, s/he really intended to do so.

Insert the following code near the top of wikiform.php:

## Add set date script to Header
$HTMLHeaderFmt['todays_date']= <<<SET_DATE
<script language="JavaScript">
<!-- Begin
function SetDate(elementName, y, m, d)
{
  // NOTE: we require that the Y/M/D elements appear
  //       in that order in the form
  var date = new Array(y, m, d);
  var index = 0;

  elements = document.getElementsByName(elementName);
  len = elements.length;

  for(i = 0; i < len; i++)
  {
    elements[i].value = String(date[index]);
    index += 1;
  }
  return;
}
//  End -->
</script>
SET_DATE;

In EntryForm(), insert the following clause:

        } elseif (preg_match('/^buttondate$/',$f[$i]['etype'],$m)) {
            if ($editing)
                $date = ($fv[$f[$i]['element']]) ? $fv[$f[$i]['element']] : 'yyyy-mm-dd';
            else
                $date = 'yyyy-mm-dd';
            $y = substr($date,0,4);
            $m = substr($date,5,2);
            $d = substr($date,8,2);
            $out[] = "<input type='text' size='5' maxlength='4' name='".
                    $f[$i]['element']."[]' value='$y' /> - ".
                    "<input type='text' size='3' maxlength='2' name='".
                    $f[$i]['element']."[]' value='$m' /> - ".
                    "<input type='text' size='3' maxlength='2' name='".
                    $f[$i]['element']."[]' value='$d' />";

            // only show "today" button if date does not yet have a value
            if ('yyyy' == $y)
            {
              $date = strftime('%Y-%m-%d',time());
              $y = substr($date,0,4);
              $m = substr($date,5,2);
              $d = substr($date,8,2);
              $element_name = $f[$i]['element'] . "[]";
              $out[] .= "<input type=button value='Today' onClick=\"SetDate('$element_name', $y, $m, $d)\">";
            }

Also, in FormData(), insert the following clause:

        elseif (preg_match('/^buttondate$/',$f[$i]['etype'])) 
            $v = str_replace('yyyy-mm-dd','',
                implode('-',$_REQUEST[$f[$i]['element']]));

shi December 20, 2006, at 02:44 PM

Added as a "today's date" checkbox in version 1.0.52 (no Javascript required). jr March 22, 2007, at 06:38 PM

attach field type

This creates a new field type 'attach' which silently adds "Attach:" markup to the input data. This makes attaching files to the page easier, since you don't have to put in the "Attach:" by hand, which is helpful for naieve users.

In EntryForm(), insert the following clause:

        } elseif (preg_match('/^attach(?:=(\\d+))?$/', $f[$i]['etype'], $m)) {
            $col = ($m[1]) ? $m[1] : 54;
            $out[] = "<input type='text' size='$col' name='".
                    $f[$i]['element']."' value='".
                    (($editing) ? DeFile($fv[$f[$i]['element']]) : $default).
                    "' />";
	}

Also, in FormData(), insert the following clause:

        elseif (preg_match('/^attach(?:=(\\d+))?$/',$f[$i]['etype'],$m)) {
            $v = $_REQUEST[$f[$i]['element']];
            if ($v) {
	    	$v = str_replace('Attach:', '', $v);
	    	$v = "Attach:$v";
	    }
	}

Kathryn Andersen February 06, 2007, at 05:39 PM

Something v similar exists at Cookbook:WikiFormsBugs#Attach Francis
If adding this feature to the core recipe, we might want to give forms administrators the option of restricting file types and generating a pick-list of allowed file types. For example, attach:pdf=32 would automatically append .pdf to the file name, with a form field 32 spaces wide. The form line generated might look like this: File name: Attach: [ . . . . . . . . . . . ].pdf (this might also display the "Attach:" prefix for additional clarity) jr March 22, 2007, at 06:38 PM
Attach field type added in version 1.0.57, includes the ability to specify a pick list of file types. jr December 13, 2007, at 09:10 PMjr

query against field values

Here is one way to implement a query to search given field values and return them in the (:wikilist ...:) list. It will result in a query that looks like this with the resulting wikilist after the "submit" is pushed:

Original Author Query:Albanian Author Query:
Original Title Query:Albanian Title Query:
Original Publisher Query:Albanian Publisher Query:
Type Query: Book
Booklet
Tract
Availability:

(Note that this example query form, above, is non-functional without the data and php code to go along with it -- this is just to give you an idea of what it looks like)

As you look at the following example here are some points to give you context:

  • This is a "database" to keep track of books (thus we are dealing with fields such as author, title, publisher, etc.)
  • These are translated books into the Albanian language, thus the opposing "original" and "Albanian" on each field
  • My main page containing the (:wikilist ...:) directive (the "query page") along with the fields to query, etc. is located at http://www.ccl-al.org/pmwiki/pmwiki.php?n=Alb.BookCatalog -- feel free to check that out if you want to see this "live"
  • The definition of the wiki fields can be found at http://www.ccl-al.org/pmwiki/pmwiki.php?n=Alb.FormTemplate -- again, feel free to check it out to see field types, etc.
  • This example is taken when I was dealing with several freeform text fields, one pulldown (where I wanted a checkbox in the query so they could choose any combination of the 3 valid values, and one checkbox where I wanted a pulldown allowing a choice of yes/no/both. The example on the live system may soon go past this...

In the page containing the (:wikilist ...:) directive (the "query page") you will have wiki source following this example:

(:messages:)\\
[+[[Add | Add a new book]]+]

(:input form "http://www.ccl-al.org/pmwiki/pmwiki.php?n=Alb.BookCatalog" method="GET":)
(:input hidden name=n "Alb.BookCatalog":)
|| border=0
|| Original Author Query:||(:input text name=origauthorquery:)|| Albanian Author Query:||(:input text name=albauthorquery:)||
|| Original Title Query:||(:input text name=origtitlequery:)|| Albanian Title Query:||(:input text name=albtitlequery:)||
|| Original Publisher Query:||(:input text name=origpubquery:)|| Albanian Publisher Query:||(:input text name=albpubquery:)||
|| Type Query:||(:input checkbox name=typebookquery value=1:) Book\\
(:input checkbox name=typebookletquery value=1:) Booklet\\
(:input checkbox name=typetractquery value=1:) Tract|| Availability:||(:input select name=availability value=1 label="BOTH in print and out of print":)
(:input select name=availability value=2 label="ONLY books still in print":)
(:input select name=availability value=3 label="ONLY books out of print":) ||

(:input submit Search:)
(:input end:)

All Items matching original author={$origauthorquery} original title={$origtitlequery} type=={$typequery} inprint=={$availablequery}
(:wikilist engauthor="{$origauthorquery}" \
           engtitle="{$origtitlequery}" \
           albauthor="{$albauthorquery}" \
           albtitle="{$albtitlequery}" \
           origpublisher="{$origpubquery}" \
           albpublisher="{$albpubquery}" \
           type="={$typequery}" \
           inprint="={$availablequery}" :)

In the local/ directory create a custom configuration file (named Alb.BookCatalog.php in my case) with this php code:

<?php
foreach ($_GET as $k=>$v) {
   $foo = htmlspecialchars($v);
   # This keeps the field values current with the form from submission to
   # submission, but has nothing to do with PTV
   $InputValues[$k] = $foo;
   # This creates a PTV
   $FmtPV['$'.$k] = "'$foo'";
}
# If all (tract/book/booklet) are blank that's equivalent to all being
# selected -- none doesn't make any sense.
if ($FmtPV['$typebookquery'] != "'1'" && $FmtPV['$typebookletquery'] != "'1'" && $FmtPV['$typetractquery'] != "'1'") {
   $FmtPV['$typebookquery'] = "'1'";
   $FmtPV['$typebookletquery'] = "'1'";
   $FmtPV['$typetractquery'] = "'1'";
   $InputValues['typebookquery'] = 1;
   $InputValues['typebookletquery'] = 1;
   $InputValues['typetractquery'] = 1;
}
# Now from 3 variables ($type . <book|booklet|tract> . query) we need
# to form one variable ($typequery) to be used in the actual query with
# a value such as "book" or "book|tract" or etc
$typequery = '';
if ($FmtPV['$typebookquery'] == "'1'") {
   $typequery = 'Book';
}
if ($FmtPV['$typebookletquery'] == "'1'") {
   if ($typequery != '') {
           $typequery .= '|';
   }
   $typequery .= "Booklet";
}
if ($FmtPV['$typetractquery'] == "'1'") {
   if ($typequery != '') {
           $typequery .= '|';
   }
   $typequery .= "Tract";
}
$FmtPV['$typequery'] = "'$typequery'";

# Now from the $availability variable we need to make a query-able variable
# called $availablequery containing either blank (out of print) or "Yes" (in print
# or "|Yes" (either in print or out of print)
if ($FmtPV['$availability'] == "'3'") {
   $FmtPV['$availablequery'] = "''";
} else {
   if ($FmtPV['$availability'] == "'2'") {
      $FmtPV['$availablequery'] = "'Yes'";
   } else {
      # this is the default - whether the value is "1" (after form submission)
      # or whether it is blank (upon initial form access)
      $FmtPV['$availablequery'] = "'|Yes'";
   }
}

Peter Bowers December 12, 2007

Note the presence of Cookbook:ProcessForm which takes care of the basic creation of PVs as well as maintaining form values between submissions. Once you have that recipe installed you can put forms like this on any page without messing with group-specific or page-specific code. The only time you would need the page-specific code would be to do various validations. Peter Bowers March 06, 2008, at 11:40 AM

Exporting to CSV or TSV

Periodically it is convenient to gather data on the web via wikiforms but then to collect it into a spreadsheet for local processing (mailmerge, etc.). Many of the things you would do with the spreadsheet could probably be done through other related recipes (publishpdf, etc.), but often the PC-based tools are more familiar to non-technical users.

This recipe is not highly polished, but it will get your data into a CSV format.

  1. Add this code to your config.php (or some other included script):
$HandleActions['export'] = 'HandleExport';
function HandleExport($pagename, $auth = 'read')
{
  global $WikiFormViewFmt, $WikiFormPageFmt;
  $group = FmtPagename('{$Group}', $pagename);
  #echo "DEBUG: group=$group<br>\n";
  $pagelist = ListPages("$group.0*");
  #echo "DEBUG: pagelist=<pre>".print_r($pagelist,true)."</pre><br>\n";
  $wikipage = $pagelist[0];
  $f = FormFields($wikipage,$WikiFormPageFmt);
  #echo "DEBUG: f=<pre>".print_r($f,true)."</pre><br>\n";
  # hdr=element --> variable name
  # hdr=eprompt --> use the prompt
  switch (@$_REQUEST['headertype']) {
      case 'var':
      case 'variable':
      case 'element':
          $hdr = 'element';
          break;
      case 'prompt':
      case 'eprompt':
          $hdr = 'eprompt';
          break;
      default:
          if (@$_REQUEST['headertype']) $hdr = $_REQUEST['headertype'];
          else $hdr = 'eprompt'; // default to using prompt
          break;
  }
  switch (@$_REQUEST['delimiter']) {
  case 'double-quote':
  case 'doublequote':
      $delimiter = '"';
      break;
  default:
      if (@$_REQUEST['delimiter']) 
          $delimiter = html_entity_decode($_REQUEST['delimiter']);
      else
          $delimiter = '"'; // default to double-quote
  }
  switch (@$_REQUEST['separator']) {
  case 'comma':
      $separator = ',';
      break;
  case 'tab':
      $separator = "\t";
      break;
  case 'semi-colon':
  case 'semi':
  case 'semicolon':
      $separator = ';';
      break;
  default:
      if (@$_REQUEST['separator']) 
          $separator = html_entity_decode($_REQUEST['separator']);
      else
          $separator = ','; // default to comma
  }
  $escaped_delimiter = $delimiter.$delimiter; // parameterize this later
  $fldlist = array();
  foreach ($f as $fields)
      $fldlist[] = $fields[$hdr];
  CSVout($fldlist, array(), $delimiter, $escaped_delimiter, $separator, "");
  foreach ($pagelist as $wikipage) {
      $fve = FormValues($wikipage,$f,'read');
#echo "DEBUG: fve=<pre>".print_r($fve,true)."</pre><br>\n";
      CSVout($fve['fv'], $f, $delimiter, $escaped_delimiter, $separator);
  }
}
function CSVout($rec, $flds, $delimiter='"', $escaped_delimiter='""', $separator=',', $init_sep="\n")
{
  $sep=$init_sep;
  $idx = 0;
  foreach ($rec as $val) {
#echo "delimiter=$delimiter, escaped_delimiter=$escaped_delimiter<br>\n";
      # Strip off the "<newline>(:title...:)" markup
      $val = html_entity_decode($val);
      if (@$flds[$idx++]['etype'] == 'title')
          $val = preg_replace("/\n\(:title .*:\)$/", '', $val);
      # Replace delimiters with double-delimiter and
      # single-quote html entity with single-quotes
      $val = str_replace(array($delimiter, 
                               '&#039;'),
                         array($escaped_delimiter, 
                               "'"),
                         $val);
      echo "$sep".$delimiter.trim($val).$delimiter;
      $sep = $separator;
  }
}
  1. On any page in the relevant group (the group containing the wikiform pages), append ?action=export to the address in the address bar. You will see a page that contains your data but looks totally messy.
  2. Do whatever you need to do in your browser to "view source" or "view page source".
    • Pressing CTRL-U (FireFox)
    • Right-clicking on the page and selecting "View Page Source"
  3. While viewing the page source, press CTRL-A to select ALL the text, then copy (CTRL-C) the text to the clipboard.
  4. Open up a local text file in an editor that handles text (Notepad or Vi or the like, not Word or Wordpad unless you already know how to "Save As" in a pure-text format. If you don't know what a "text" format is then choose Notepad.)
  5. Paste the text from step #4 into your editor.
  6. Save it as a text file with a ".csv" extension (something like wikiformdata.csv). (Remember where you put it!)
  7. Open your spreadsheet program and Open that file (you may have to indicate that the "File Type" is CSV in order to see it)
    • Note that OpenOffice Calc (an open source spreadsheet program) does a far superior job in importing CSV files than Excel. Particularly if you have any fields with multiple lines Excel will not properly import those records.
    • You will need to set the options appropriately on the import. If you are using the defaults from this export recipe then you will need to choose a comma as the field separator (not tab or semi-colon or etc.) and you will need to choose double-quote as a field delimiter.
    • If you are unfamiliar with importing CSV data then it would be wise to do a quick Google search on "CSV import tutorial" or something like that. More detail than is supplied here is beyond the scope of this recipe.
  8. You now have your data in a spreadsheet and can sort it, mailmerge it, analyze your data, etc. ENJOY!
Note: In step #2 above, you can specify several other options to change the format of the exported data (each option specified would begin with an ampersand (&)):
Option NameDefaultExplanation
headertype=<variable|prompt>promptThis determines what value will be used in the first row of your data. prompt uses your prompt from FormTemplate while variable uses the variable name.
delimiter=<doublequote|OTHER>doublequoteWhat will be used to delimit each field. You can specify any string.
separator=<comma|tab|semicolon|OTHER>commaWhat character/string will be used to separate one field from another. The 3 values specified are known by their names (comma=, and etc.) but other characters/strings can be specified by entering their literal values (separator=^^^)
escaped_delimiter=<NOT IMPLEMENTED>double-delimiterThis option can be easily implemented, but is not yet done. Currently whatever you specify as your delimiter (doublequote by default) will be doubled (2 doublequotes in a row) in order to specify that delimiter within your data.

Obviously if you change options to reformat your data then your import into your spreadsheet will have to be similarly modified.

There's some way to bypass the saving into a CSV file -- just pasting directly into the spreadsheet and then executing some command. Unfortunately I don't remember -- feel free to modify this description if you know how to do that.

Peter Bowers April 24, 2011, at 09:52 AM