]> sipb.mit.edu Git - ikiwiki.git/blob - doc/todo/structured_page_data.mdwn
response
[ikiwiki.git] / doc / todo / structured_page_data.mdwn
1 This is an idea from [[JoshTriplett]].  --[[Joey]]
2
3 Some uses of ikiwiki, such as for a bug-tracking system (BTS), move a bit away from the wiki end
4 of the spectrum, and toward storing structured data about a page or instead
5 of a page. 
6
7 For example, in a bug report you might want to choose a severity from a
8 list, enter a version number, and have a bug submitter or owner recorded,
9 etc. When editing online, it would be nice if these were separate fields on
10 the form, rather than the data being edited in the big edit form.
11
12 There's a tension here between remaining a wiki with human-editable source
13 files, containing freeform markup, and more structured data storage. I
14 think that it would be best to include the structured data in the page,
15 using a directive. Something like:
16
17         part of page content
18         \[[data yaml="<arbitrary yaml here>"]]
19         rest of page content 
20
21 As long as the position of the directive is not significant, it could be
22 stripped out when web editing, the yaml used to generate/populate form fields, 
23 and then on save, the directive regenerated and inserted at top/bottom of
24 the page.
25
26 Josh thinks that yaml is probably a good choice, but the source could be a
27 `.yaml` file that contains no directives, and just yaml. An addition
28 complication in this scenario is, if the yaml included wiki page formatted content,
29 ikiwiki would have to guess or be told what markup language it used.
30
31 Either way, the yaml on the page would encode fields and their current content.
32 Information about data types would be encoded elsewhere, probably on a
33 parent page (using a separate directive). That way, all child pages could
34 be forced to have the same fields.
35
36 There would be some simple types like select, boolean, multiselect, string, wiki markup.
37 Probably lists of these (ie, list of strings). Possibly more complex data
38 structures.
39
40 It should also be possible for plugins to define new types, and the type
41 definitions should include validation of entered data, and how to prompt
42 the user for the data.
43
44 This seems conceptually straightforward, if possibly quite internally
45 complex to handle the more complicated types and validation.
46
47 One implementation wrinkle is how to build the html form. The editpage.tmpl
48 currently overrides the standard [[!cpan CGI::FormBuilder]] generated form,
49 which was done to make the edit page be laid out in a nice way. This,
50 however, means that new fields cannot be easily added to it using
51 [[!cpan CGI::FormBuilder]]. The attachment plugin uses the hack of bouilding
52 up html by hand and dumping it into the form via a template variable. 
53
54 It would be nice if the type implementation code could just use
55 FormBuilder, since its automatic form generation, and nice field validation
56 model is a perfect match for structured data. But this problem with
57 editpage.tmpl would have to be sorted out to allow that.
58
59 Additional tie-ins:
60
61 * Pagespecs that can select pages with a field with a given value, etc.
62   This should use a pagespec function like field(fieldname, value).  The
63   semantics of this will depend on the type of the field; text fields will
64   match value against the text, and link fields will check for a link
65   matching the pagespec value.
66 * The search plugin could allow searching for specific fields with specific
67   content. (xapian term search is a good fit).
68
69 See also:
70
71 [[tracking_bugs_with_dependencies]]
72
73 > I was also thinking about this for bug tracking.  I'm not sure what
74 > sort of structured data is wanted in a page, so I decided to brainstorm
75 > use cases:
76 >
77 > * You just want the page to be pretty.
78 > * You want to access the data from another page.  This would be almost like
79 >     like a database lookup, or the OpenOffice Calc [VLookup](http://wiki.services.openoffice.org/wiki/Documentation/How_Tos/Calc:_VLOOKUP_function) function.
80 > * You want to make a pagespec depend upon the data.  This could be used
81 >    for dependancy tracking - you could match against pages listed as dependencies,
82 >    rather than all pages linked from a given page.
83 >
84 >The first use case is handled by having a template in the page creation.  You could
85 >have some type of form to edit the data, but that's just sugar on top of the template.
86 >If you were going to have a web form to edit the data, I can imagine a few ways to do it:
87 >
88 > * Have a special page type which gets compiled into the form.  The page type would
89 >    need to define the form as well as hold the stored data.
90 > * Have special directives that allow you to insert form elements into a normal page.
91 >
92 >I'm happy with template based page creation as a first pass...
93 >
94 >The second use case could be handled by a regular expression directive. eg:
95 >
96 > \[[regex spec="myBug" regex="Depends: ([^\s]+)"]]
97 >
98 > The directive would be replaced with the match from the regex on the 'myBug' page... or something.
99 >
100 >The third use case requires a pagespec function.  One that matched a regex in the page might work.
101 >Otherwise, another option would be to annotate links with a type, and then check the type of links in
102 >a pagespec.  e.g. you could have `depends` links and normal links.
103 >
104 >Anyway, I just wanted to list the thoughts.  In none of these use cases is straight yaml or json the
105 >obvious answer.  -- [[Will]]
106
107 >> Okie.  I've had a play with this.  A 'form' plugin is included inline below, but it is only a rough first pass to
108 >> get a feel for the design space.
109 >>
110 >> The current design defines a new type of page - a 'form'.  The type of page holds YAML data
111 >> defining a FormBuilder form.  For example, if we add a file to the wiki source `test.form`:
112
113     ---
114     fields:
115       age:
116         comment: This is a test
117         validate: INT
118         value: 15
119
120 >> The YAML content is a series of nested hashes.  The outer hash is currently checked for two keys:
121 >> 'template', which specifies a parameter to pass to the FromBuilder as the template for the
122 >> form, and 'fields', which specifies the data for the fields on the form.
123 >> each 'field' is itself a hash.  The keys and values are arguments to the formbuilder form method.
124 >> The most important one is 'value', which specifies the value of that field.
125 >>
126 >> Using this, the plugin below can output a form when asked to generate HTML.  The Formbuilder
127 >> arguments are sanitized (need a thorough security audit here - I'm sure I've missed a bunch of
128 >> holes).  The form is generated with default values as supplied in the YAML data.  It also has an
129 >> 'Update Form' button at the bottom.
130 >>
131 >>  The 'Update Form' button in the generated HTML submits changed values back to IkiWiki.  The
132 >> plugin captures these new values, updates the YAML and writes it out again.  The form is
133 >> validated when edited using this method.  This method can only edit the values in the form.
134 >> You cannot add new fields this way.
135 >>
136 >> It is still possible to edit the YAML directly using the 'edit' button.  This allows adding new fields
137 >> to the form, or adding other formbuilder data to change how the form is displayed.
138 >>
139 >> One final part of the plugin is a new pagespec function.  `form_eq()` is a pagespec function that
140 >> takes two arguments (separated by a ',').  The first argument is a field name, the second argument
141 >> a value for that field.  The function matches forms (and not other page types) where the named
142 >> field exists and holds the value given in the second argument.  For example:
143     
144     \[[!inline pages="form_eq(age,15)" archive="yes"]]
145     
146 >> will include a link to the page generated above.
147
148 >>> Okie, I've just made another plugin to try and do things in a different way.
149 >>> This approach adds a 'data' directive.  There are two arguments, `key` and `value`.
150 >>> The directive is replaced by the value.  There is also a match function, which is similar
151 >>> to the one above.  It also takes two arguments, a key and a value.  It returns true if the
152 >>> page has that key/value pair in a data directive.  e.g.:
153
154     \[[!data key="age" value="15"]]
155
156 >>> then, in another page:
157
158     \[[!inline pages="data_eq(age,15)" archive="yes"]]
159
160 >>> I expect that we could have more match functions for each type of structured data,
161 >>> I just wanted to implement a rough prototype to get a feel for how it behaves.  -- [[Will]]
162
163 >> Anyway, here are the plugins.  As noted above these are only preliminary, exploratory, attempts. -- [[Will]]
164
165     #!/usr/bin/perl
166     # Interpret YAML data to make a web form
167     package IkiWiki::Plugin::form;
168     
169     use warnings;
170     use strict;
171     use CGI::FormBuilder;
172     use IkiWiki 2.00;
173     
174     sub import { #{{{
175         hook(type => "getsetup", id => "form", call => \&getsetup);
176         hook(type => "htmlize", id => "form", call => \&htmlize);
177         hook(type => "sessioncgi", id => "form", call => \&cgi_submit);
178     } # }}}
179     
180     sub getsetup () { #{{{
181         return
182                 plugin => {
183                         safe => 1,
184                         rebuild => 1, # format plugin
185                 },
186     } #}}}
187     
188     sub makeFormFromYAML ($$$) { #{{{
189         my $page = shift;
190         my $YAMLString = shift;
191         my $q = shift;
192     
193         eval q{use YAML};
194         error($@) if $@;
195         eval q{use CGI::FormBuilder};
196         error($@) if $@;
197         
198         my ($dataHashRef) = YAML::Load($YAMLString);
199         
200         my @fields = keys %{ $dataHashRef->{fields} };
201         
202         unshift(@fields, 'do');
203         unshift(@fields, 'page');
204         unshift(@fields, 'rcsinfo');
205         
206         # print STDERR "Fields: @fields\n";
207         
208         my $submittedPage;
209         
210         $submittedPage = $q->param('page') if defined $q;
211         
212         if (defined $q && defined $submittedPage && ! ($submittedPage eq $page)) {
213                 error("Submitted page doensn't match current page: $page, $submittedPage");
214         }
215         
216         error("Page not backed by file") unless defined $pagesources{$page};
217         my $file = $pagesources{$page};
218         
219         my $template;
220         
221         if (defined $dataHashRef->{template}) {
222                 $template = $dataHashRef->{template};
223         } else {
224                 $template = "form.tmpl";
225         }
226         
227         my $form = CGI::FormBuilder->new(
228                 fields => \@fields,
229                 charset => "utf-8",
230                 method => 'POST',
231                 required => [qw{page}],
232                 params => $q,
233                 action => $config{cgiurl},
234                 template => scalar IkiWiki::template_params($template),
235                 wikiname => $config{wikiname},
236                 header => 0,
237                 javascript => 0,
238                 keepextras => 0,
239                 title => $page,
240         );
241         
242         $form->field(name => 'do', value => 'Update Form', required => 1, force => 1, type => 'hidden');
243         $form->field(name => 'page', value => $page, required => 1, force => 1, type => 'hidden');
244         $form->field(name => 'rcsinfo', value => IkiWiki::rcs_prepedit($file), required => 1, force => 0, type => 'hidden');
245         
246         my %validkey;
247         foreach my $x (qw{label type multiple value fieldset growable message other required validate cleanopts columns comment disabled linebreaks class}) {
248                 $validkey{$x} = 1;
249         }
250     
251         while ( my ($name, $data) = each(%{ $dataHashRef->{fields} }) ) {
252                 next if $name eq 'page';
253                 next if $name eq 'rcsinfo';
254                 
255                 while ( my ($key, $value) = each(%{ $data }) ) {
256                         next unless $validkey{$key};
257                         next if $key eq 'validate' && !($value =~ /^[\w\s]+$/);
258                 
259                         # print STDERR "Adding to field $name: $key => $value\n";
260                         $form->field(name => $name, $key => $value);
261                 }
262         }
263         
264         # IkiWiki::decode_form_utf8($form);
265         
266         return $form;
267     } #}}}
268     
269     sub htmlize (@) { #{{{
270         my %params=@_;
271         my $content = $params{content};
272         my $page = $params{page};
273     
274         my $form = makeFormFromYAML($page, $content, undef);
275     
276         return $form->render(submit => 'Update Form');
277     } # }}}
278     
279     sub cgi_submit ($$) { #{{{
280         my $q=shift;
281         my $session=shift;
282         
283         my $do=$q->param('do');
284         return unless $do eq 'Update Form';
285         IkiWiki::decode_cgi_utf8($q);
286     
287         eval q{use YAML};
288         error($@) if $@;
289         eval q{use CGI::FormBuilder};
290         error($@) if $@;
291         
292         my $page = $q->param('page');
293         
294         return unless exists $pagesources{$page};
295         
296         return unless $pagesources{$page} =~ m/\.form$/ ;
297         
298         return unless IkiWiki::check_canedit($page, $q, $session);
299     
300         my $file = $pagesources{$page};
301         my $YAMLString = readfile(IkiWiki::srcfile($file));
302         my $form = makeFormFromYAML($page, $YAMLString, $q);
303     
304         my ($dataHashRef) = YAML::Load($YAMLString);
305     
306         if ($form->submitted eq 'Update Form' && $form->validate) {
307                 
308                 #first update our data structure
309                 
310                 while ( my ($name, $data) = each(%{ $dataHashRef->{fields} }) ) {
311                         next if $name eq 'page';
312                         next if $name eq 'rcs-data';
313                         
314                         if (defined $q->param($name)) {
315                                 $data->{value} = $q->param($name);
316                         }
317                 }
318                 
319                 # now write / commit the data
320                 
321                 writefile($file, $config{srcdir}, YAML::Dump($dataHashRef));
322     
323                 my $message = "Web form submission";
324     
325                 IkiWiki::disable_commit_hook();
326                 my $conflict=IkiWiki::rcs_commit($file, $message,
327                         $form->field("rcsinfo"),
328                         $session->param("name"), $ENV{REMOTE_ADDR});
329                 IkiWiki::enable_commit_hook();
330                 IkiWiki::rcs_update();
331     
332                 require IkiWiki::Render;
333                 IkiWiki::refresh();
334     
335                 IkiWiki::redirect($q, "$config{url}/".htmlpage($page)."?updated");
336     
337         } else {
338                 error("Invalid data!");
339         }
340     
341         exit;
342     } #}}}
343     
344     package IkiWiki::PageSpec;
345     
346     sub match_form_eq ($$;@) { #{{{
347         my $page=shift;
348         my $argSet=shift;
349         my @args=split(/,/, $argSet);
350         my $field=shift @args;
351         my $value=shift @args;
352     
353         my $file = $IkiWiki::pagesources{$page};
354         
355         if ($file !~ m/\.form$/) {
356                 return IkiWiki::FailReason->new("page is not a form");
357         }
358         
359         my $YAMLString = IkiWiki::readfile(IkiWiki::srcfile($file));
360     
361         eval q{use YAML};
362         error($@) if $@;
363     
364         my ($dataHashRef) = YAML::Load($YAMLString);
365     
366         if (! defined $dataHashRef->{fields}->{$field}) {
367                 return IkiWiki::FailReason->new("field '$field' not defined in page");
368         }
369     
370         my $formVal = $dataHashRef->{fields}->{$field}->{value};
371     
372         if ($formVal eq $value) {
373                 return IkiWiki::SuccessReason->new("field value matches");
374         } else {
375                 return IkiWiki::FailReason->new("field value does not match");
376         }
377     } #}}}
378     
379     1
380
381 ----
382
383     #!/usr/bin/perl
384     # Allow data embedded in a page to be checked for
385     package IkiWiki::Plugin::data;
386     
387     use warnings;
388     use strict;
389     use IkiWiki 2.00;
390     
391     sub import { #{{{
392         hook(type => "getsetup", id => "data", call => \&getsetup);
393         hook(type => "needsbuild", id => "data", call => \&needsbuild);
394         hook(type => "preprocess", id => "data", call => \&preprocess, scan => 1);
395     } # }}}
396     
397     sub getsetup () { #{{{
398         return
399                 plugin => {
400                         safe => 1,
401                         rebuild => 1, # format plugin
402                 },
403     } #}}}
404     
405     sub needsbuild (@) { #{{{
406         my $needsbuild=shift;
407         foreach my $page (keys %pagestate) {
408                 if (exists $pagestate{$page}{data}) {
409                         if (exists $pagesources{$page} &&
410                             grep { $_ eq $pagesources{$page} } @$needsbuild) {
411                                 # remove state, it will be re-added
412                                 # if the preprocessor directive is still
413                                 # there during the rebuild
414                                 delete $pagestate{$page}{data};
415                         }
416                 }
417         }
418     }
419     
420     sub preprocess (@) { #{{{
421         my %params=@_;
422     
423         $pagestate{$params{page}}{data}{$params{key}} = $params{value};
424         
425         return IkiWiki::preprocess($params{page}, $params{destpage}, 
426                 IkiWiki::filter($params{page}, $params{destpage}, $params{value})) if defined wantarray;
427     } # }}}
428     
429     
430     package IkiWiki::PageSpec;
431     
432     sub match_data_eq ($$;@) { #{{{
433         my $page=shift;
434         my $argSet=shift;
435         my @args=split(/,/, $argSet);
436         my $key=shift @args;
437         my $value=shift @args;
438     
439         my $file = $IkiWiki::pagesources{$page};
440         
441         if (! exists $IkiWiki::pagestate{$page}{data}) {
442                 return IkiWiki::FailReason->new("page does not contain any data directives");
443         }
444         
445         if (! exists $IkiWiki::pagestate{$page}{data}{$key}) {
446                 return IkiWiki::FailReason->new("page does not contain data key '$key'");
447         }
448         
449         my $formVal = $IkiWiki::pagestate{$page}{data}{$key};
450     
451         if ($formVal eq $value) {
452                 return IkiWiki::SuccessReason->new("value matches");
453         } else {
454                 return IkiWiki::FailReason->new("value does not match");
455         }
456     } #}}}
457     
458     1