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):

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 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.
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:
That's a very high level list, but enough to get us started.
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.
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.
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:
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:
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!
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:
There, I've said it. Onward.
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:
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.
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>
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.
| Date | Author | Description |
| 14-October-2008 | neilw | Revised Path argument processing |
| 6-October-2008 | neilw | First version posted |
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:
Let's apply these to our ForumTopicList template right now.
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.
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.
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>
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.
What exactly are we trying to accomplish here?
The existing data structures won't give us any of this, so we'll need assemble this ourselves.
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 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;
}
}
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.
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.
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:
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("Create New Topic",homepath,nil,true,"Put Your Title Here")}}</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') { " by "; web.link(t.author.uri, t.author.name); } " (";t.type;")"; }}</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>
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.
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.
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. My approach will simply be to go through the template code and explain what is being done. 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. 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. There are three steps to be done for each subpage that is processed. 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. 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.Introduction
History
Date Author Description
6-November-2008 CraigSivils First version posted Processing a Path Argument
<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
var postcountmap = {};
var postcountlist = [];
Generating the post count numbers
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
let postcountlist = list.sort(map.keyvalues(postcountmap), 'value', true);
}">
Displaying the output
<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>
The final source code is shown below. See it in operation here.Putting it all together
<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>