How do I... Output a group of wiki pages using DekiScript?

You can use a DekiScript template when you want to output a group of pages as a single page.

 

First: let's create a template for our purposes; we are calling the template "MakeBook".  Paste the following code in source view of the Template.

<div block="var pth = $0 ?? $path ?? page.path; var tree = wiki.tree(pth); var pg = wiki.getpage(pth);">
<div class="hideonprintpdf" if="$1 ?? $include_toc">
<h1>{{ wiki.localize(&quot;Skin.Common.header-toc&quot;) }}</h1>
{{ wiki.toc(pth) }}</div>
<div if="$2 ?? $include_root">
<h1>{{ pg.title }}</h1>
{{ wiki.page(pth) }}</div>
<div foreach="var a in tree['//a']" block="var p = wiki.getpage(num.cast(xml.text(a, '@pageid')));">
<div style="page-break-after: always;"><span style="display: none;">&nbsp;</span></div>
{{ var heading = #p.parents-#pg.parents; var s = '&lt;h' .. heading .. '&gt;' .. p.title .. '&lt;/h' .. heading .. '&gt;'; web.html(s); }} {{ wiki.page(p.path) }}
<div if="($3 ?? $include_files) &amp;&amp; #p.files &gt; 0">
<h2 style="color: red;">File Attachments</h2>
<p>{{ #p.files; &quot; files are attached to page &quot;; p.title }}</p>
<ol> <li foreach="var file in p.files"><strong>{{ file.name }}</strong><br /> <em>Description:</em> {{ file.description }}<br /> <em>Date:</em> {{ file.date }}<br /> <em>Site:</em> {{ file.size }}<br /> <em>URI:</em> {{ web.link(file.uri) }}<br /> <em>Mime:</em> {{ file.mime }}</li> <ol>&nbsp;</ol> </ol> </div> </div> </div>

All the parameters are optional:

  • path: Page path to output tree content, by default the current page.
  • include_root: Include the content of the root page, by default false.
  • include_toc: Include a Table of contents, by default true;

 

Usage:

{{ template.MakeBook{ path: "DekiScript/Tutorials/Building_a_Discussion_Forum", include_toc: true, include_root: true, include_files: true } }}

Output:


Table of contents

Building a Discussion Forum

Introduction

This four-part tutorial explains how to build a simple but remarkably functional discussion forum in pure Dekiscript, using only two templates and a modest chunk of code.  If you can make it through all four parts and understand all the code, you'll have a pretty complete understanding of Dekiscript.

In general, this tutorial assumes a reasonable level of familiarity with Dekiscript, though it'll occasionally dive into some more basic material.  Useful more advanced tips may also be sprinkled throughout.

Here's a screenshot of the finished product  (red outline added for clarity):

forum_example.png

Credits

The original forum code was created by craigsivils, who therefore inspired this insanity.  Craig has also contributed to its ongoing development.

Additional development, templatification, and tutorial by neilw.

Thanks (or perhaps, not) to SteveB for suggesting the tutorial.

The Tutorial

  • Part 1: Basic Structure - This first part introduces the basic structure of the ForumTopicList template we'll be creating.  This is a common Dekiscript pattern that is useful in many applications, and will form the basis for our forums.
  • Part 2: Arguments and refinements - Here we add some useful (and highly reusable) argument processing to the template, and then focus on refining the topic list to make it operate more like a real forum.
  • Part 3: The ForumTopic Template (not available yet) - Here we'll extend the functionality of our forum list by introducing a template for each topic, and taking advantage of this to put more useful information on the forum topic listing.  The final code presented in part 3 is a usable, fully functioning discussion forum.
  • Part 4: Gilding the Lilly (not available yet) - In our interest of exploring every nook and cranny of Dekiscript, for the grand finale we will add some functionality that is not really necessary but very cool.  Check it out!

 

 

Building a Discussion Forum, Part 1: Basic Structure

The first part of this tutorial will lay down the basic structure of our ForumTopicList template.  When finished, we'll have a barebones forum structure in place, just enough to whet our appetite.  Our approach is applicable to a wide variety of Deki applications, so this will provide a useful foundation.

Requirements

So what exactly is a discussion forum and what should it do?  Essentially, we'll try to duplicate a useful subset of the functionality of widely used discussion forums such as PHPBB or VBulletin.  For a typical example, just check out the Mindtouch forums.

So here's a quick rundown of our requirements:

  1. For now, we want to provide the functionality of a single forum.
  2. We should support basic threading.  This means that at the top level, we have a list of topics, and each topic has its own stream of replies.
  3. It should be easy to create new topics and replies.
  4. Our topic list should be sorted in reverse chronological order (i.e., most recently updated first.)

That's a very high level list, but enough to get us started.

Should we use templates?

Yes!  In general, it is much better to hide re-usable code in a template.  We'll provide as much customizability as we can by passing arguments to the template (see the second part for the gory details.)

So we'll call our first template "ForumTopicList".  As the name suggests, this will generate a list of forum topics (amazing!).  Our basic call, using all default arguments, will be:

{{ template("ForumTopicList") }}

We'll refer to the page on which the above code is inserted as the calling page.

The Basic Structure

Now let's talk about the basic structure that, as previously promised, is very generally applicable to a wide variety of uses.  Deki is going to make our lives easy on one important respect, which is that each page can have a string of comments attached to the bottom.  Therefore, it would make sense for each topic to be its own wiki page, and then the reply mechanism is handled automatically.

Page Hierarchy

We must then decide where to store the topic pages.  We could put them directly underneath the calling page, but then we'd run the risk of the forum topics being intermingled with other, "real" subpages of the calling page.  Instead, we'll create a subpage underneath the calling page called "Forum Topics", and put our topics underneath there.  This has two advantages:

  1. Our forum topics are safely sequestered, away from other "content" pages.
  2. We know that all pages underneath the "Forum Topics" page are indeed forum topics.  Therefore we don't have to sort through them and make decisions.

We'll refer to the "Forum Topics" page as the "homepage" in our scripts.

The first piece of code we can create, then, is a button to create a new topic:

{{wiki.create("Create New Topic",(page.path.."/Forum_Topics"),nil,true,"Put Your Title Here")}}

Notes:

  1. Here I've used the "arg-list" form for calling wiki.create().  I could just as easily have used the "map" form, though it would have been a bit more verbose.  Throughout this tutorial, I'll freely switch back and forth between the two, as the situation dictates.  For more information on this topic, see this FAQ entry.
  2. In creating the "path" argument for wiki.create(), we've replaced the space in "Forum Topics" with an underscore.  Different Deki versions may or may not accept spaces in a path, so we'll be safe and use the underscore, which should work in all versions.
  3. For now, we've not specified a template for the new topic page.  We'll get to that later.
  4. By naming the new page "Put your title here", we're saying that the page title will be the "Subject" of the topic.  This is a natural approach, and from a scripting standpoint it's easy to work with the title of the page, so this works out well overall.

Initializing Variables in a <div block="">

Something looks wrong in the wiki.create() code above.  We've embedded an expression for the path to our homepage in the code.  We're going to need to use that path (lets call it "homepath") elsewhere in the code as well, so we'd like to set it once and then reuse it.  But those double-curly braces define there own scope, so we can't set a variable in there and expect to see it anywhere else.  The solution lies in using HTML statements: any variables set in a "block" attribute are visible anywhere within that element.  In order to make our initialized variables visible to the entire template, we wrap the entire template in a <div> and put our initialization code in a "block" attribute.  Diving into the source editor, it looks like this:

<h1>Template:ForumTopicList</h1>
<div block="var homepath = page.path .. '/Forum_Topics';"> ...the rest of the page contents...
</div>

This is a mighty useful technique that I recommend highly.  By putting all common code in the surrounding <div>, you make it easy to find, and the rest of the code inside the the template becomes much cleaner and simpler.  Note that the div code is invisible in the WYSIWYG editor.  We'll be spending most of our time in the source editor anyway.

Our updated button code now looks like this (back in the WYSIWYG editor):

{{wiki.create("Create New Topic",homepath,nil,true,"Put Your Title Here")}}

Much better!

A Quick and Necessary Digression on the Travails of Working in the Source Editor

The forum templates are going to involve a lot of work in the source editor.  Unfortunately, because the source editor is not designed expressly for Dekiscript-inside-HTML, it's not a particularly pleasant place to hang out.  Especially treacherous are misplaced double-quotes and less-than or greater-than signs (<>).

When exiting out of the source view into the WYSIWYG view, the editor parses your HTML and "cleans it up".  That includes removing anything it doesn't like.  No warnings, no opportunity to go back and undo.  This can be a bummer when a misplaced quote causes 90% of your code to be unrecoverably discarded in the transition from source view back to WYSIWYG view.  Therefore, here are a few techniques I've adopted to maintain sanity:

  1. Check, double-check, and triple check your code for placement of quotes, less-than, and greater-than signs before going back to WYSIWYG.  Remember that if you wrap your attribute code in double quotes, then you must only use single quotes inside your code.
  2. Manually encode these symbols (e.g., "&lt;", "&gt;", and "&quot;") inside your Dekiscript whenever possible.  Don't wait for the source editor to do it.
  3. Avoid those symbols in your Dekiscript whenever possible.  For instance, I'd rather use "variable != 0" than "variable > 0" because the latter will eventually end up as the much-less-readable "variable &gt; 0".
  4. After editing your code and getting ready to go back to WYSIWYG mode, do a "select all" (ctrl-A) and "copy" (ctrl-C).  That way, if your code gets hosed in the transition, you can paste it back into the source editor and try to find the error.
  5. Any time you edit code and exit the source editor, immediately go back in and check that it looks the same as you left it.  Do this always!

There, I've said it.  Onward.

Generating the List of Topics

Finally, let's generate the list of topics.  This will be a table we'll generate with HTML statements.  The format can be anything you want; for now, we'll use a fairly standard forum layout, with columns for Topic, Author, Replies, Last Update, and Views (though I am finding the "Views" column to be of somewhat limited value).

We'll iterate the <tr> element over the subpages of our homepage.  That'll look something like this:

<tr foreach="var p in wiki.getpage(homepath).subpages" if="wiki.getpage(homepath)">
<td>{{ web.link(p.uri, p.title) }}</td> <td>{{ web.link(p.author.uri, p.author.name) }}</td> <td>{{ #p.comments }}</td> <td>{{ date.format(p.date,'yyyy-M-d H:mm').." by "..web.link(p.author.uri, p.author.name) }}</td> <td>{{ p.viewcount }}</td> </tr>

Note the "if" clause which saves us from a bunch of errors if the homepage hasn't been created yet.

There are lots of problems with this code:

  1. It doesn't sort the items by most recent date.
  2. It doesn't properly attribute the original author of the topic.  Instead, "p.author" will return the author of the most recent revision.  In fact, the second column lists the same author as the "last Update" column.
  3. It also doesn't properly attribute the author of the "Last Update" if that update is a comment.  "p.author" only tells you who made the last change to the page content.
  4. If there aren't any topics, it just puts out a null list, which isn't very friendly.
  5. The "foreach" clause and "if" clause contain duplicate code ("wiki.getpage(homepath)").  This looks like a good candidate for the initialization code in the <div block="">.

Number 5 is trivial so we'll fix it now.  #1, #3, and #4 will be tackled in part 2.  #2 is a bit thornier; we'll get back to it in part 3.

Results

Final partly-functional code for part 1, with some gussied-up formatting, is presented below, and attached to this page.  See it in action here.

<h1>Template:TopicForumList part1</h1>
<div block="var homepath = page.path..'/Forum_Topics';
var homepage = wiki.getpage(homepath);">
<p>{{wiki.create("Create New Topic",homepath,nil,true,"Put Your Title Here")}}</p>
<table cellspacing="0" cellpadding="3" border="1" class="table" style="border-collapse: separate;">
    <tbody>
        <tr>
            <th valign="top" style="width: 55%;">Topic</th>
            <th valign="top" style="width: 10%; text-align: center;">Author</th>
            <th valign="top" style="width: 5%; text-align: center;">Replies</th>
            <th valign="top" style="width: 25%; text-align: center;">Last Update</th>
            <th valign="top" style="width: 5%;">Views</th>
        </tr>
        <tr if="homepage" foreach="var p in homepage.subpages">
            <td valign="top">{{ web.link(p.uri, p.title) }}</td>
            <td style="vertical-align: top; text-align: center;">{{ web.link(p.author.uri, p.author.name) }}</td>
            <td style="vertical-align: top; text-align: center;">{{ #p.comments }}</td>
            <td valign="top" style="text-align: center;">{{ date.format(p.date,'yyyy-M-d H:mm'); " by "; web.link(p.author.uri, p.author.name) }}</td>
            <td valign="top" style="text-align: center;">{{ p.viewcount }}</td>
        </tr>
    </tbody>
</table>
</div>

File Attachments

1 files are attached to page Building a Discussion Forum, Part 1: Basic Structure

  1. ForumTopicList_part1.html
    Description: Final source code for this part of the tutorial
    Date: Fri, 19 Sep 2008 20:12:23 GMT
    Site: 1329
    URI: http://developer.mindtouch.com/@api/deki/files/3341/=ForumTopicList_part1.html
    Mime: text/html
    1.  
 

Building a Discussion Forum, Part 2: Arguments and Refinements

Introduction

In part one of this tutorial we laid out a basic structure for managing a list of pages in Dekiscript.  While the result looks a lot like a forum topic listing, it's still lacking sufficient functionality to really think of it as a forum.  In this part we'll add some refinement to move us along that path, and discuss some useful argument-passing techniques.

History

Date Author Description
14-October-2008 neilw Revised Path argument processing
6-October-2008 neilw First version posted

 

Processing a Path Argument

Principles of Template Arguments

Processing arguments in a template is simple, and is a very valuable way to make the template useful in a wide variety of situations.  As always, though, you must find a balance between providing overly restrictive, hard-wired code and providing so many options that your code becomes a mess.  Here are my general principles of template arguments:

  1. Nothing hard-wired, within reason: If possible, no values should be hard-wired in your template.  In some unusual situations, generalizing may be tough to code, and you'll have to decide whether it's worth the time.  But in general, you should look out for anything that will needlessly restrict the use of the template.  Remember: most users will not have the ability to customize your template with different values.  They'll just skip it and move on.  In my recent experience, I am always surprised at some of the things users want to be customizable.  More flexibility is almost always better.
  2. Useful defaults: The template should be useful if no arguments are provided.
  3. Simple usage: The format of passed arguments should be as simple as possible for the user.  Many wiki users who wish to use your template will not be Dekiscript-savvy, so you want to minimize the complexity of the format of your arguments.  This may require a bit of extra code in your template, but it is well worth it.

Let's apply these to our ForumTopicList template right now.

The Path argument

The most notable thing in our code from part 1 is that the homepath is hardwired to a particular subpage ("Forum_Topics") underneath the current page.  That's probably a useful default for 90% (or even more) of real-world usage, but there's no reason to leave it hardwired.  One good example of when it would be necessary to change it would be when the user embedded more than one forum on the same page.  In that case, each forum would need its own subpage.

So, let's add some arg processing code to our initialization block.  We could just do the minimum:

var homepath = (args.path ?? page.path..'/Forum_Topics');

That kinda works.  But what happens if the user wants a different subpage of the current page?  Then their template call would have to look like this:

{{ template("ForumTopicList", { path:path.."/My_Forum_Topics" }) }}

That's a little gross for the casual user.  Fortunately, we can make it better.

Improved Path Processing

We know that for this application, the most common path argument is probably going to be a subpage underneath the current page, and we'd like to save the user the need to put in the "page.path..".  So let's define a convenient Unix-like convention: any path argument specified with a leading "./" will be relative to the current page.  Otherwise, the path is assumed to be fully specified (e.g., from root).  In that case, the user above would simply need to offer:

{{ template("ForumTopicList", { path:"./My_Forum_Topics" }) }}

That's much cleaner.  Now let's code it up:

var homepath = (args.path ?? './Forum_Topics');
if (string.startswith(homepath, './') { let homepath = page.path..string.substr(homepath,1); }

Simple and effective.  I use this code in almost every template.  Technically, the wiki ought to support some form of relative path syntax, but in the meantime this is trivial to implement and does the job.

Checking for empty

One of our uses for homepath is to get the correspond page using wiki.getpage(), and then iterating on that page's subpages.  It's possible that page doesn't exist; after all, it'll only be created after the first topic is created (unless the user manually creates it first.)  If the page doesn't exist, then wiki.getpage() will return nil, and trying to get the subpages of nil will throw an error.  Furthermore, it would be nice to know when there are no forum topics yet posted, so we could put up a nice message to that effect, rather than just an empty table.  Let's fix both problems at once.

First, as before, we want to fetch the page variable for homepath:

var homepage = wiki.getpage(homepath);

Now, we'll add some checking.  Let's define a boolean variable empty which is true if there are no topics (for whatever reason):

var empty = (homepage == nil || #homepage.subpages == 0);

This enables us to improve our table content generator as follows:

<tr if="!empty" foreach="var p in homepage.subpages">
...
</tr>
<tr if="empty">
  <td colspan="5">(no topics)</td>
</tr>

 

Sorting the List of Topics

Our code is looking nicer, but we still haven't implemented the one thing that makes a forum a forum: reverse-chronological sorting of topics, based on the date of most recent update to each one.  Let's tackle that now.

Requirements

What exactly are we trying to accomplish here?

  1. Sort the list of topics in reverse-chronological order, based on time of last update.
  2. Define last update as either most recent comment, or most recent edit to the page, whichever came last.
  3. Show the time and author of the last update.
  4. Identify the nature of the last update.

The existing data structures won't give us any of this, so we'll need assemble this ourselves.

Creating and Sorting a List of Maps

Deki gives us the set of topics in the form of a map; in our case, it's homepage.subpages.  In order to enable the creation of a sorted list, we need to put that data in a different form.  A list of maps will be ideal, primarily because the list.sort() function provides the ability to sort a list of maps based on any field in the map.  In our case we'll want a field that specifies the date of last update, in sortable format.  Let's construct our list, which we'll call topiclist, in our initialization block as follows:

var topiclist = [];
if (!empty) {
  foreach (var p in homepage.subpages) {
    var lastupdate = <still need to figure this out>;
    var lastauthor = <this too>;
    let topiclist ..= [ { page:p, date:date, author:lastauthor } ];
  }
  let topiclist = list.sort(topiclist, 'date', true);
}

We anticipated our needs a bit, including a field for the author of the last update, since that's not provided in the page object (at least not the way we define "last author").  Now we just need to figure out lastupdate and lastauthor

Finding the Last Author and Time

Finding the author and time of the last update turns out to be surprisingly easy: take the most recent comment (if there is one), and compare its timestamp to the time of last update to the page.  Use whichever is more recent.  All this data is conveniently provided in the page object, so our work is simple:

var lastupdate = page.date;
var lastauthor = page.author;
if (#page.comments != 0) {
  var lastcomment = page.comments[#page.comments-1];
  if (date.isafter(lastcomment.date, lastupdate)) {
    let lastupdate = lastcomment.date;
    let lastauthor = lastcomment.author;
  }
}

 

Type of last update

We said we wanted to note the type of last update, so we'll add just a bit of code for that.  Our code structure above makes it pretty easy for us to note whether it's a page edit or a comment; the one thing remaining is to determine if it's a new page.  That's most easily determined by looking at page.editsummary; if it's a new page, then the first part of the summary will be "page created".  So we'll add a new field to the map called type, and set it to the text we want to display for each update type.  The final code for the list generation and sorting looks like this:

var topiclist = [];
if (!empty) {
  foreach (var p in homepage.subpages) {
    var lastupdate = p.date;
    var lastauthor = p.author;
    var lasttype = (string.substr(p.editsummary, 0, 12) == 'page created' ? 'new' : 'E');
    if (#p.comments != 0) {
      var lastcomment = p.comments[#p.comments-1];
      if (date.isafter(last.comment.date, lastupdate)) {
        let lastupdate = lastcomment.date;
        let lastauthor = lastcomment.author;
        let lasttype = 'C';
      }
    }
    let topiclist ..= [ { page:p, author:lastauthor, type:lasttype, date:date.format(lastupdate, 's') } ];
  }
  let topiclist = list.sort(topiclist, 'date', true);
}

One final comment: when we filled in the map for each item in topiclist, we changed the format of the date to make it sortable.  The date.format() function offers any .NET date format; specifying 's' is a handy shortcut for a sortable format.  It's not very pretty to look at, but we can always reformat it later for presentation.  The important thing is that it makes our sorting work correctly.

Results

Finally, we tweak our presentation a bit, adjusting for the fact that we're now going to iterate on topiclist, and get some of our info from each map element. In the WYSIWYG editor, the table code looks like this.  Note the changes from the first part.

forum_example_2.PNG

The final source code, shown below, is almost a working forum.  In fact, if you're not too demanding, you could certainly use this code successfully.  See it in operation here.  There are really only two things lacking:

  1. We still don't know who the originator of each topic is.
  2. Each topic page is a completely blank slate, lacking some useful information that forums typically provide.

We'll tackle both of these in part 3.

<h1>Template:ForumTopicList tutorial part2</h1> 
  <div block="var homepath = ($0 ?? args.path ?? './Forum_Topics');
if (string.substr(homepath,0,2) == './') { let homepath = page.path..string.substr(homepath,1); }
var homepage = wiki.getpage(homepath);
var empty = (homepage == nil || #homepage.subpages == 0);
var topiclist = [];
if (!empty) {
  foreach (var p in homepage.subpages) {
    var lastupdate = p.date;
    var lastauthor = p.author;
    var lasttype = (string.substr(p.editsummary, 0, 12) == 'page created' ? 'new' : 'E');
    if (#p.comments != 0) {
      var lastcomment = p.comments[#p.comments-1];
      if (date.isafter(lastcomment.date, lastupdate)) {
        let lastupdate = lastcomment.date;
        let lastauthor = lastcomment.author;
        let lasttype = 'C';
      }
    }
    let topiclist ..= [ { page:p, author:lastauthor, type:lasttype, date:date.format(lastupdate, 's') } ];
  }
  let topiclist = list.sort(topiclist, 'date', true);
}"> 
    <p>{{wiki.create(&quot;Create New Topic&quot;,homepath,nil,true,&quot;Put Your Title Here&quot;)}}</p> 
    <table border="1" cellpadding="3" cellspacing="0" class="table" style="border-collapse: separate; "> 
      <tbody> 
        <tr> 
          <th style="width: 55%; " valign="top">Topic</th> 
          <th style="width: 10%; text-align: center; " valign="top">Author</th> 
          <th style="width: 5%; text-align: center; " valign="top">Replies</th> 
          <th style="width: 25%; text-align: center; " valign="top">Last Comment(C) or Edit(E)</th> 
          <th style="width: 5%; " valign="top">Views</th> 
        </tr> 
        <tr foreach="var t in topiclist" if="!empty"> 
          <td style="font-weight: bold; " valign="top"><font style="font-size: 17px; ">{{ web.link( t.page.uri, t.page.title) }}</font></td> 
          <td style="vertical-align: top; text-align: center; ">{{ web.link(t.page.author.uri, t.page.author.name) }}</td> 
          <td style="vertical-align: top; text-align: center; ">{{ #t.page.comments }}</td> 
          <td style="text-align: center; " valign="top"><font style="font-size: 12px; ">{{ date.format(t.date,'yyyy-M-d H:mm'); if (t.type != 'new') { &quot; by &quot;; web.link(t.author.uri, t.author.name); } &quot; (&quot;;t.type;&quot;)&quot;; }}</font></td> 
          <td style="text-align: center; " valign="top">{{ t.page.viewcount }}</td> 
        </tr> 
        <tr if="empty"> 
          <td colspan="5">(no topics)</td> 
        </tr> 
      </tbody> 
    </table> 
  </div>

 

File Attachments

2 files are attached to page Building a Discussion Forum, Part 2: Arguments and Refinements

  1. ForumTopicList_part2.html
    Description:
    Date: Fri, 17 Oct 2008 01:46:56 GMT
    Site: 2512
    URI: http://developer.mindtouch.com/@api/deki/files/3455/=ForumTopicList_part2.html
    Mime: text/html
  2. forum_example_2.PNG
    Description:
    Date: Mon, 06 Oct 2008 13:18:03 GMT
    Site: 19085
    URI: http://developer.mindtouch.com/@api/deki/files/3454/=forum_example_2.PNG
    Mime: image/png
    1.  
 

Building a Discussion Forum, Part 3: The ForumTopic Template

UNDER CONSTRUCTION

As you can see, this part of the tutorial is not ready yet.  I'll post a notice to the forum when it is.

 

Building a Discussion Forum, Part 4: Gilding the Lilly

UNDER CONSTRUCTION

As you can see, this part of the tutorial is not ready yet.  I'll post a notice to the forum when it is.

 

Building a Discussion Forum, Part 5: Coding silly stuff just because

Introduction

This portion of the tutorial demonstrates some advanced use of maps and the xml.text function to retrieve content from subpages.  I am adding the tutorial here because it avoids having to create a complex data structure simply to demonstrate how to aggregate data from that structure.  Special thanks to NeilW for cleaning up the code, and to CRB for helping me with an xpath example.

History

Date Author Description
6-November-2008 CraigSivils First version posted

 

My approach will simply be to go through the template code and explain what is being done.

Processing a Path Argument

The code begins by processing an argument "homepath" with the location of the forum posts.  This is the standard code described in section 2 of the tutorial.

<div block="var homepath='Templates/Basic_Threaded_Discussion_Forum/Forum_Topics';
if (homepath[0] == '/') {
  let homepath = page.path .. homepath;
}
var homepage = wiki.getpage(homepath);
var empty = (homepage == nil || #homepage.subpages == 0);

Initialize List and Map variables

The post count code uses both a list and a map to produce the output.  A map is used because it allows for elements to be dynamically created and modified.  Once the count has been generated in the map, the information is moved to a list to sort the results in descending order.  The variable postcountmap contains the map data and the variable postcountlist contains the list data.

var postcountmap = {};
var postcountlist = [];

Generating the post count numbers

There are three steps to be done for each subpage that is processed.

  1. The contents of the subpage is retrieved (Required for step 2)
  2. The originator tag is pulled from the contents of the subpage using the xml.text command with an xpath string (argument 2).  The ?? operator is used to provide an alternate value if the originator tag could not be found.
  3. The postcountmap is updated to tally one more post for the originator.  The map contents uses the name of the originator as the key and the value is the number of posts by that user.  The ?? operator is also used for the map assignment since the first time a post is found for each user, the map will not have an entry for that user.
if (!empty) { foreach (var p in homepage.subpages)
{
  var entry = wiki.page(p.path);
  var originator = (xml.text(entry, '//*[@id=\'ForumEntryOriginator\']/a') ?? p.author.name);
  let postcountmap ..= { (originator):1+(postcountmap[originator]??0) }; 
}

Sort the output

The post count map is now transfered to a list which is sorted using the list.sort function.  This can be done in one step as shown below.  Since this is the last step in the inital processing, the code block is closed.

let postcountlist = list.sort(map.keyvalues(postcountmap), 'value', true);
}">

Displaying the output

A simple table is used to display the output data.  Neilw's empty trick is also used to handle the situation that there were no posts to count.

    <table cellspacing="0" cellpadding="4" border="1" class="table"> 
      <tbody> 
        <tr> 
          <th valign="top">Author</th> 
          <th valign="top">Posts</th> 
        </tr> 
        <tr foreach="var p in postcountlist" if="!empty"> 
          <td>{{ p.key }}</td> 
          <td>{{ p.value }}</td> 
        </tr> 
        <tr if="empty"> 
          <td colspan="2">(no topics yet)</td> 
        </tr> 
      </tbody> 
    </table><br /> 
  </div>

Putting it all together

The final source code is shown below.  See it in operation here.

<h1>Template:ForumPostCount</h1> 
  <div block="var homepath='Templates/Basic_Threaded_Discussion_Forum/Forum_Topics';
if (homepath[0] == '/') {
  let homepath = page.path .. homepath;
}
var homepage = wiki.getpage(homepath);
var empty = (homepage == nil || #homepage.subpages == 0);
var postcountmap = {};
var postcountlist = [];
if (!empty) { foreach (var p in homepage.subpages)
{
  var entry = wiki.page(p.path);
  var originator = (xml.text(entry, '//*[@id=\'ForumEntryOriginator\']/a') ?? p.author.name);
  let postcountmap ..= { (originator):1+(postcountmap[originator]??0) };
}
  let postcountlist = list.sort(map.keyvalues(postcountmap), 'value', true);
}"> 
    <table cellspacing="0" cellpadding="4" border="1" class="table"> 
      <tbody> 
        <tr> 
          <th valign="top">Author</th> 
          <th valign="top">Posts</th> 
        </tr> 
        <tr foreach="var p in postcountlist" if="!empty"> 
          <td>{{ p.key }}</td> 
          <td>{{ p.value }}</td> 
        </tr> 
        <tr if="empty"> 
          <td colspan="2">(no topics yet)</td> 
        </tr> 
      </tbody> 
    </table><br /> 
  </div>

Tag page
Viewing 5 of 5 comments: view all
I'm confused by this line of code: web.html('$title' % p); How does '$title'%p evaluate? I can't find this in the Dekiscript syntax.
Posted 20:00, 27 Aug 2008
It seems to work the same as addressing a map but you're telling the parser to pull them all from the same variable. Try this in your wiki: {{ web.html('<p>Title = $title, Path = $path, Count = $viewcount</p>' % page) }}
It should automatically extract page.title, page.path, and page.viewcount and display them... I guess this would help some with readability when using the same variable multiple times in a single string instead of doing 'text'..var.sub..'text'..var.sub2..'text'..var.sub3..'text'
Posted 00:17, 27 Sep 2008
It's based on string.format. So instead of doing string.format("$a $b", { a : 1, b: 2 }), you can just write "$a $b" % { a : 1, b: 2 }. Since the page object is also a map, you can pass it instead of an explicit one, like "$a $b" % c.
Posted 10:35, 27 Sep 2008
I'm a newbe to scipting, and I'm having trouble using the script where you show how to use wiki.tree and wiki.page to show all context of af Wiki hompage. I get error: /block/div/block/div/div/span, function 'wiki.page' failed with response:missing value for parameter 'path' (index 0), when im using the template. Any ideas ? .. Thx.
Posted 14:03, 12 Nov 2008
That's very useful. I am making a glossary node in which each child page contains a term and definition. Using your example, I can now easily generate a single page with all entries. Well done.
Posted 08:01, 31 Dec 2008
Viewing 5 of 5 comments: view all
You must login to post a comment.