X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/eee22b3c5194383b7f54eba835a3b597816609b0..f49d15649f62ec695b6f0ca20f7fc1b042a09af3:/doc/todo/structured_page_data.mdwn?ds=sidebyside

diff --git a/doc/todo/structured_page_data.mdwn b/doc/todo/structured_page_data.mdwn
index a8f8d2108..9f21fab7f 100644
--- a/doc/todo/structured_page_data.mdwn
+++ b/doc/todo/structured_page_data.mdwn
@@ -1,5 +1,7 @@
 This is an idea from [[JoshTriplett]].  --[[Joey]]
 
+* See further discussion at [[forum/an_alternative_approach_to_structured_data]].
+
 Some uses of ikiwiki, such as for a bug-tracking system (BTS), move a bit away from the wiki end
 of the spectrum, and toward storing structured data about a page or instead
 of a page. 
@@ -82,6 +84,10 @@ See also:
 >    rather than all pages linked from a given page.
 >
 >The first use case is handled by having a template in the page creation.  You could
+
+
+
+
 >have some type of form to edit the data, but that's just sugar on top of the template.
 >If you were going to have a web form to edit the data, I can imagine a few ways to do it:
 >
@@ -104,7 +110,7 @@ See also:
 >Anyway, I just wanted to list the thoughts.  In none of these use cases is straight yaml or json the
 >obvious answer.  -- [[Will]]
 
->> Okie.  I've had a play with this.  A plugin is included inline below, but it is only a rough first pass to
+>> Okie.  I've had a play with this.  A 'form' plugin is included inline below, but it is only a rough first pass to
 >> get a feel for the design space.
 >>
 >> The current design defines a new type of page - a 'form'.  The type of page holds YAML data
@@ -144,8 +150,113 @@ See also:
     \[[!inline pages="form_eq(age,15)" archive="yes"]]
     
 >> will include a link to the page generated above.
->>
->> Anyway, here is the plugin.  As noted above this is only a preliminary, exploratory, attempt. -- [[Will]]
+
+>>> Okie, I've just made another plugin to try and do things in a different way.
+>>> This approach adds a 'data' directive.  There are two arguments, `key` and `value`.
+>>> The directive is replaced by the value.  There is also a match function, which is similar
+>>> to the one above.  It also takes two arguments, a key and a value.  It returns true if the
+>>> page has that key/value pair in a data directive.  e.g.:
+
+    \[[!data key="age" value="15"]]
+
+>>> then, in another page:
+
+    \[[!inline pages="data_eq(age,15)" archive="yes"]]
+
+>>> I expect that we could have more match functions for each type of structured data,
+>>> I just wanted to implement a rough prototype to get a feel for how it behaves.  -- [[Will]]
+
+>> Anyway, here are the plugins.  As noted above these are only preliminary, exploratory, attempts. -- [[Will]]
+
+>>>> I've just updated the second of the two patches below.  The two patches are not mutually
+>>>> exclusive, but I'm leaning towards the second as more useful (for the things I'm doing). -- [[Will]]
+
+I think it's awesome that you're writing this code to explore the problem
+space, [[Will]] -- and these plugins are good stabs at at least part of it.
+Let me respond to a few of your comments.. --[[Joey]]
+
+On use cases, one use case is a user posting a bug report with structured
+data in it. A template is one way, but then the user has to deal with the
+format used to store the structured data. This is where a edit-time form
+becomes essential.
+
+> This was the idea with the 'form' plugin.  With the 'data' plugin I was exploring
+> a different approach: try to keep the markup simple enough that the user can edit
+> the markup directly, and still have that be ok.  I admit it is a stretch, but I thought
+> it worth exploring.
+
+Another use case is, after many such bugs have been filed,
+wanting to add a new field to each bug report. To avoid needing to edit
+every bug report it would be good if the fields in a bug report were
+defined somewhere else, so that just that one place can be edited to add
+the new field, and it will show up in each bug report (and in each bug
+report's edit page, as a new form field).
+
+> If I was going to do that, I'd use a perl script on a checked out
+> workspace.  I think you're describing a rare operation and
+> so I'd be happy not having a web interface for it.  Having said that,
+> if you just wanted to change the form for *new* pages, then you
+> can just edit the template used to create new pages.
+
+Re the form plugin, I'm uncomfortable with tying things into
+[[!cpan CGI::FormBuilder]] quite so tightly as you have.
+
+> Yeah :).  But I wanted to explore the space and that was the
+> easiest way to start.
+
+CGI::FormBuilder
+could easily change in a way that broke whole wikis full of pages. Also,
+needing to sanitize FormBuilder fields with security implications is asking
+for trouble, since new FormBuilder features could add new fields, or
+add new features to existing fields (FormBuilder is very DWIM) that open
+new security holes. 
+
+> There is a list of allowed fields.  I only interpret those.
+
+I think that having a type system, that allows defining specific types,
+like "email address", by writing code (that in turn can use FormBuilder),
+is a better approach, since it should avoid becoming a security problem.
+
+> That would be possible.  I think an extension to the 'data' plugin might
+> work here.
+
+One specific security hole, BTW, is that if you allow the `validate` field,
+FormBuilder will happily treat it as a regexp, and we don't want to expose
+arbitrary perl regexps, since they can at least DOS a system, and can
+probably be used to run arbitrary perl code.
+
+> I validate the validate field :).  It only allows validate fields that match
+> `/^[\w\s]+$/`.  This means you can really only use the pre-defined
+> validation types in FormBuilder.
+
+The data plugin only deals with a fairly small corner of the problem space,
+but I think does a nice job at what it does. And could probably be useful
+in a large number of other cases.
+
+> I think the data plugin is more likely to be useful than the form plugin.
+> I was thinking of extending the data directive by allowing an 'id' parameter.
+> When you have an id parameter, then you can display a small form for that
+> data element.  The submission handler would look through the page source
+> for the data directive with the right id parameter and edit it.  This would
+> make the data directive more like the current 'form' plugin.
+
+> That is making things significantly more complex for less significant gain though. --[[Will]]
+
+> Oh, one quick other note.  The data plugin below was designed to handle multiple
+> data elements in a single directive.  e.g.
+
+    \[[!data key="Depends on" link="bugs/bugA" link="bugs/bugB" value=6]]
+
+> would match `data_eq(Depends on,6)`, `data_link(Depends on,bugs/bugA)`, `data_link(Depends on,bugs/bugB)`
+> or, if you applied the patch in [[todo/tracking_bugs_with_dependencies]] then you can use 'defined pagespecs'
+> such as `data_link(Depends on,~openBugs)`. <a id="another_kind_of_links" />The ability to label links like this allows separation of
+> dependencies between bugs from arbitrary links.
+>> This issue (the need for distinguished kinds of links) has also been brought up in other discussions: [[tracking_bugs_with_dependencies#another_kind_of_links]] (deps vs. links) and [[tag_pagespec_function]] (tags vs. links). --Ivan Z.
+
+>>> And multiple link types are now supported; plugins can set the link
+>>> type when registering a link, and pagespec functions can match on them. --[[Joey]] 
+
+----
 
     #!/usr/bin/perl
     # Interpret YAML data to make a web form
@@ -156,21 +267,21 @@ See also:
     use CGI::FormBuilder;
     use IkiWiki 2.00;
     
-    sub import { #{{{
+    sub import {
     	hook(type => "getsetup", id => "form", call => \&getsetup);
     	hook(type => "htmlize", id => "form", call => \&htmlize);
     	hook(type => "sessioncgi", id => "form", call => \&cgi_submit);
-    } # }}}
+    }
     
-    sub getsetup () { #{{{
+    sub getsetup () {
     	return
     		plugin => {
     			safe => 1,
     			rebuild => 1, # format plugin
     		},
-    } #}}}
+    }
     
-    sub makeFormFromYAML ($$$) { #{{{
+    sub makeFormFromYAML ($$$) {
     	my $page = shift;
     	my $YAMLString = shift;
     	my $q = shift;
@@ -249,9 +360,9 @@ See also:
     	# IkiWiki::decode_form_utf8($form);
     	
     	return $form;
-    } #}}}
+    }
     
-    sub htmlize (@) { #{{{
+    sub htmlize (@) {
     	my %params=@_;
     	my $content = $params{content};
     	my $page = $params{page};
@@ -259,9 +370,9 @@ See also:
     	my $form = makeFormFromYAML($page, $content, undef);
     
     	return $form->render(submit => 'Update Form');
-    } # }}}
+    }
     
-    sub cgi_submit ($$) { #{{{
+    sub cgi_submit ($$) {
     	my $q=shift;
     	my $session=shift;
     	
@@ -324,11 +435,11 @@ See also:
     	}
     
     	exit;
-    } #}}}
+    }
     
     package IkiWiki::PageSpec;
     
-    sub match_form_eq ($$;@) { #{{{
+    sub match_form_eq ($$;@) {
     	my $page=shift;
     	my $argSet=shift;
     	my @args=split(/,/, $argSet);
@@ -359,6 +470,164 @@ See also:
     	} else {
     		return IkiWiki::FailReason->new("field value does not match");
     	}
-    } #}}}
+    }
+    
+    1
+
+----
+
+    #!/usr/bin/perl
+    # Allow data embedded in a page to be checked for
+    package IkiWiki::Plugin::data;
+    
+    use warnings;
+    use strict;
+    use IkiWiki 2.00;
+    
+    my $inTable = 0;
+    
+    sub import {
+    	hook(type => "getsetup", id => "data", call => \&getsetup);
+    	hook(type => "needsbuild", id => "data", call => \&needsbuild);
+    	hook(type => "preprocess", id => "data", call => \&preprocess, scan => 1);
+    	hook(type => "preprocess", id => "datatable", call => \&preprocess_table, scan => 1);	# does this need scan?
+    }
+    
+    sub getsetup () {
+    	return
+    		plugin => {
+    			safe => 1,
+    			rebuild => 1, # format plugin
+    		},
+    }
+    
+    sub needsbuild (@) {
+    	my $needsbuild=shift;
+    	foreach my $page (keys %pagestate) {
+    		if (exists $pagestate{$page}{data}) {
+    			if (exists $pagesources{$page} &&
+    			    grep { $_ eq $pagesources{$page} } @$needsbuild) {
+    				# remove state, it will be re-added
+    				# if the preprocessor directive is still
+    				# there during the rebuild
+    				delete $pagestate{$page}{data};
+    			}
+    		}
+    	}
+    }
+    
+    sub preprocess (@) {
+    	my @argslist = @_;
+    	my %params=@argslist;
+    	
+    	my $html = '';
+    	my $class = defined $params{class}
+    			? 'class="'.$params{class}.'"'
+    			: '';
+    	
+    	if ($inTable) {
+    		$html = "<th $class >$params{key}:</th><td $class >";
+    	} else {
+    		$html = "<span $class >$params{key}:";
+    	}
+    	
+    	while (scalar(@argslist) > 1) {
+    		my $type = shift @argslist;
+    		my $data = shift @argslist;
+    		if ($type eq 'link') {
+    			# store links raw
+    			$pagestate{$params{page}}{data}{$params{key}}{link}{$data} = 1;
+    			my $link=IkiWiki::linkpage($data);
+    			add_depends($params{page}, $link);
+    			$html .= ' ' . htmllink($params{page}, $params{destpage}, $link);
+    		} elsif ($type eq 'data') {
+    			$data = IkiWiki::preprocess($params{page}, $params{destpage}, 
+    				IkiWiki::filter($params{page}, $params{destpage}, $data));
+    			$html .= ' ' . $data;
+    			# store data after processing - allows pagecounts to be stored, etc.
+    			$pagestate{$params{page}}{data}{$params{key}}{data}{$data} = 1;
+    		}
+    	}
+    		
+    	if ($inTable) {
+    		$html .= "</td>";
+    	} else {
+    		$html .= "</span>";
+    	}
+    	
+    	return $html;
+    }
+    
+    sub preprocess_table (@) {
+    	my %params=@_;
+    
+    	my @lines;
+    	push @lines, defined $params{class}
+    			? "<table class=\"".$params{class}.'">'
+    			: '<table>';
+    
+    	$inTable = 1;
+    
+    	foreach my $line (split(/\n/, $params{datalist})) {
+    		push @lines, "<tr>" . IkiWiki::preprocess($params{page}, $params{destpage}, 
+    			IkiWiki::filter($params{page}, $params{destpage}, $line)) . "</tr>";
+    	}
+    
+    	$inTable = 0;
+    
+    	push @lines, '</table>';
+    
+    	return join("\n", @lines);
+    }
+    
+    package IkiWiki::PageSpec;
+    
+    sub match_data_eq ($$;@) {
+    	my $page=shift;
+    	my $argSet=shift;
+    	my @args=split(/,/, $argSet);
+    	my $key=shift @args;
+    	my $value=shift @args;
+    
+    	if (! exists $IkiWiki::pagestate{$page}{data}) {
+    		return IkiWiki::FailReason->new("page does not contain any data directives");
+    	}
+    	
+    	if (! exists $IkiWiki::pagestate{$page}{data}{$key}) {
+    		return IkiWiki::FailReason->new("page does not contain data key '$key'");
+    	}
+    	
+    	if ($IkiWiki::pagestate{$page}{data}{$key}{data}{$value}) {
+    		return IkiWiki::SuccessReason->new("value matches");
+    	} else {
+    		return IkiWiki::FailReason->new("value does not match");
+    	}
+    }
+    
+    sub match_data_link ($$;@) {
+    	my $page=shift;
+    	my $argSet=shift;
+    	my @params=@_;
+    	my @args=split(/,/, $argSet);
+    	my $key=shift @args;
+    	my $value=shift @args;
+    
+    	if (! exists $IkiWiki::pagestate{$page}{data}) {
+    		return IkiWiki::FailReason->new("page $page does not contain any data directives and so cannot match a link");
+    	}
+    	
+    	if (! exists $IkiWiki::pagestate{$page}{data}{$key}) {
+    		return IkiWiki::FailReason->new("page $page does not contain data key '$key'");
+    	}
+    	
+    	foreach my $link (keys %{ $IkiWiki::pagestate{$page}{data}{$key}{link} }) {
+    		# print STDERR "Checking if $link matches glob $value\n";
+    		if (match_glob($link, $value, @params)) {
+    			return IkiWiki::SuccessReason->new("Data link on page $page with key $key matches glob $value: $link");
+    		}
+    	}
+    
+    	return IkiWiki::FailReason->new("No data link on page $page with key $key matches glob $value");
+    }
     
     1