FIXME ADD:
- create admin interface to explain:
- write your own macros & filterss!!
- DONE url mapping
- DONE method dispatching
- middleware (auth)
- DONE more involved actions. request object, different response
- filestore relations for comments -> post
- file upload. image for blog entries
- logging, log who created / modified what post
- unit tests
- small ringo shell script for.. some blog cleanup task i will have to invent
- extending java, simple package create walkthrough
WIP, http://gist.github.com/399012
This tutorial is written for the most
recent version as available from git.
I will walk you through the creation of a basic blog. A assume you already have Ringo installed. See [Getting Started] if you need help with that.
The final, refined app developed in this tutorial is on github http://github.com/oberhamsi/ringo-tutorial-demoblog.
If you run into trouble do not hesitate to ask for help on Ringo's Mailinglist or you can join us on IRC #ringojs on irc.freenode.net.
General
Model Layer
- Store Setup
- Excursion: Relational Databases
- Creating & Storing Model Instances
- Querying
- Excursion: Batteries included
View Layer
- Actions and Skins
- Skins: Macros and Filters
- URL Patterns: Capturing Arguments for Actions
- RESTful Dispatch on Method
- The Request Object
Other Batteries
First, let Ringo's admin-create
command take care of creating the directory structure and a couple of files that every webapp needs:
$ ringo-admin create demoblog
This will create the demoblog
folder containing a functional - but not yet very useful - web application. This is a template for your app to get you started. But it is already a runnable web app as you will soon see.
admin-create
created a couple of files in the top level directory:
-
main.js
The script bootstrapping your web application. -
config.js
Settings for middleware, database, the app itself, etc. -
actions.js
Functions creating the Response. The View in MVC.
.. and several directories with rather self explanatory names:
-
skins/
Put all your skins (templates) in here. -
static/
By default Ringo serves files from this directory under the URL /static/ -
config/
Holds jetty configuration files. Do not worry about those.
Calling ringo main.js
will start a jetty webserver on your machine and you can view the demoblog app right away in your browser.
$ cd demoblog
$ ringo main.js
This will start a server on http://127.0.0.1:8080.
Instead of typing ringo main.js
all the time you can start any web application with the ringo-web
command. config.js
must be in the directory from which you call ringo-web
:
$ cd demoblog
$ ringo-web
If the server did not start that is probably because the default port used by Ringo (8080) is already in use. You can change the port in config.js
by modifing the httpConfig.port
property:
// config.js
exports.httpConfig = {
staticDir: 'static',
port: '8787',
};
This is also where you can change the directory from which Ringo serves the static files - leave it as "static" for this tutorial.
Ringo follows the [CommonJs] Standard. You will first notice this when dealing with modules. There are multiple JavaScript module patterns out there but in RingoJs you should only use this one:
- Every file is a module living in its own top-level scope. No special syntax needed.
- Attach to
exports
any functions or other properties a module should expose. require('foobar')
returns an object holding all exported properties of the module foobar.include('foobar')
brings all the exported properties of the module foobar into the including module (by convention: only useinclude
in the shell)
An example should make things clearer. A simple module in the file foobar.js
might look like the following. I want to exposes the function add
but not the private adder
:
// foobar.js
var adder = function(a, b) {
return a + b;
};
exports.add = function(a, b) {
return adder(a, b);
};
You can then either include()
that module, which would instantly make add
available:
>> include('foobar');
>> add(2,3)
5
Or - to keep your namespace clean - you can require()
it and then access add
as foobar.add
:
>> var foobar = require('foobar');
>> foobar.add(3,4)
7
Another option is to use destructuring assignment to extract exactly the properties you want from the module you require:
>> var {add} = require('foobar');
>> add(2,3)
5
See [Modules in RingoJs] or the post RingoJS Modules and how to fix the Global Object for more details.
Ringo ships with multiple store backends. We will us the filestore
for this tutorial because it does not need a database backend. Filestore simply puts all its data into the directory we tell it to.
We setup the Store in config.js
by requiring the appropriate storage module and instantiating a Store. Other modules will import this config module to get access to the store, therefor we must export the store property by attaching it to exports
.
Store
only takes one argument - the filesystem path where it should save its data. You better change storePath
to an absolute path like /home/yourname/db.demoblog/
.
// config.js
var filestore = require('ringo/storage/filestore');
var storePath = 'db';
exports.store = new filestore.Store(storePath);
Now that we have a store setup, we can start defining the entities we want to put into the store.
By convention you should put all model code into model.js
. Create that file now in your demoblog folder. We will put all data describing the models as well as any model behaviour into that module.
We import the config
module to get access to the store instance we setup earlier. The filestore, like every store, has the function defineEntity()
which allows us to, well, define entities that can be put into or retrieved from the store. How many arguments and what kind of arguments defineEntity()
accepts depends on the store implementation. The filestore only requires one argument: the name of the entity.
The filestore is a schemaless store, which means we do not have to define in advance what properties the entities have; we can simply attach properties to entity instances and whatever we attach will get stored. In a traditional, schemafull store (like MySQL) you would have declare in advance - when creating tables - what properties ("fields") the entities ("tables") have.
// model.js
var config = require('./config');
exports.Post = config.store.defineEntity('Post');
exports.Comment = config.store.defineEntity('Comment');
That's it. defineEntity()
returns the constructor for the entities, which we in turn export so other modules can access them. We will later use the Post
and Comment
constructors to create Posts and Comments.
For a more traditional, schemafull store, like MySql, model.js
would be the place to setup the whole database mapping. Typically, you would map every table in your database to a store entity, and the fields of the table would map to properties of the entities.
To give you a taste: in case your using something like MySQL your model definition will look more like the following:
// model.js (do not use this for tutorial. this is just an example)
var Post = store.defineEntity('Post', {
table: 'persons',
properties: {
title: {type: 'string', nullable: false},
text: {type: 'string', nullable: false},
createtime: {type: 'timestamp', nullable: false},
}
});
.. and you would have to instantiate a different store in config.js
namely Ringo's Relational Store which you can install with ringo-admin. See [How to install packages][Package_Management] if you need more help with that.
$ ringo-admin install robi42/ringo-hibernate
ringo-hibernate is work in progress and does not have much documentation. You can most easily find out what is currently possible by checking out ringo-hiberante's unit tests.
There are in fact other store implementation, check out the Database section in the [list of available packages for Ringo][Packages].
Let's do something with our models. We will create some blog posts in the Ringo shell. Change into the demoblog directory and start the shell:
$ cd demoblog
$ ringo
Import the model
module. If you made any mistakes in config.js
or model.js
they will show up now as errors. We can now use the constructor functions we exported in the model
module to create new Posts:
>> include('./model');
>> var post = new Post()
>> post.author = 'Simon'
Simon
>> post.createtime = new Date()
Tue May 11 2010 13:47:51 GMT+0200 (CEST)
>> post.text = 'My first blog Post!'
My first blog Post!
That was easy, but the Post is not yet persisted! Instances of store entities have a save()
function that takes care of that:
>> post.save()
There is also a remove()
function which drops the entity from the store, e.g.:
>> post.remove() // would remove that post from the store
Its usually nicer to pass the constructor an object holding all the properties you want stored:
>> var post = new Post({
.. title: 'Second Post',
.. author: 'simon',
.. text: 'Follow up post in which i explain the first Post.',
.. createtime: new Date()
.. })
>> secondPost.save()
Oh no, the first Post does not have a title. Time to query that post and give it a title!
When we defined the store entities in model.js
, Ringo did something behind our backs: defineEntity()
not only gave us back a constructor for creating the entities Post and Comment, it also added the query()
function to each of those.
We can do Post.query()
which does nothing, but allows us to chain real query functions. As many as we want. Currently Ringo supports the following query functions, all chainable:
- equals (property, value)
- greater (property, value)
- greaterEquals (property, value)
- less (property, value)
- lessEquals (property, value)
To get all Posts by a certain author you would use query()
and chain an equals()
query and then select()
to actually get the list of matching entities:
>> Post.query().equals('author', 'simon').select()
[object Storable],[object Storable]
Or to get all Posts from that author before a certain date, just add another query call:
>> Post.query().equals('author', 'simon').
.. less('createtime', new Date()).select()
[object Storable],[object Storable]
But once you call select()
on a query-chain Ringo will return the list of Entities matching the Queries.
In addition to query()
all store entities have get(id)
, so we can for example do Post.get(1)
to get the post with id 1.
All those [object Storable]
are ugly. We should add a nicer string representation for Posts. Even if this is just for our own development sanity.
We will add a toString
function to the Post protototype that will return a pretty string representation. Ringo comes with many useful Modules, we are going to use ringo/utils/dates
here to format the date:
// model.js
var dates = require('ringo/utils/dates');
Post.prototype.toString = function() {
return '[Post: ' + this.title + ' (' + this.author + ', ' +
dates.format(this.createtime, 'dd.MM.yyyy') + ')]';
};
Ringo ships with many useful modules. We have JSON support, a module for file operations, a unit testing framework and more. Most of the interesting stuff is in the ringo/ namespace.
If you still do not find what you need maybe someone else has already written a packacke that might help you: List of Ringo Packages.
Now we can more easily tell which Entity needs fixing. We query the Posts again but first reload the module by include'ing it again:
>> include('model')
>> Post.query().select()
[Post: second post (simon, 11.05.2010)],[Post: undefined (simon, 11.05.2010)]
Pretty. Though now the missing title
is glaring. Fixing that is easy. We set the title
attribute on the second post and save()
it:
>> var posts = Post.query().select()
>> posts[1].title = 'Introductory Post'
Introductory Post
>> posts[1].save()
Now both Posts show up nicely.
>> Post.query().select()
[Post: Second Post (simon, 11.05.2010)],[Post: Introductory Post (simon, 11.05.2010)]
Time to publish our thoughts!
The current index
function in actions.js
only returns a static skin. We will extend the function to load the last ten Posts and pass them to the skin for rendering. At the top we require the model
module we wrote, so we get access to the Post
prototype. As before we use query()
to select all posts - we are only interested in the first ten so we slice them off.
var posts = model.Post.query().select().slice(0,10);
After that we use skinResponse()
to return the rendered html to the browser. skinResponse()
expects two arguments: the path to the skin file to render and a object with arbitrary properties. We call the later the "skin context". All the properties defined in the skin context are available for scripting in the skin. I'll show you in a minute. But this is how our action, returning the rendered skin, looks like:
// actions.js
var response = require('ringo/webapp/response');
var model = require('./model');
exports.index = function index(req) {
var posts = model.Post.query().select().slice(0,10);
return response.skinResponse('skins/index.html', {
posts: posts,
});
};
In the skin we will now access the "posts" array from the context and render a subskin for each post. The "posts" array is accessed as <% posts %>
in the skin. You could just put something like this into the skin:
// in a skin
<% posts %>
This would output the same string representation of the list of posts as we would get on the shell:
// rendered skin output of <% posts %>
[Post: Second Post (simon, 11.05.2010)], [Post: Introductory Post (simon, 11.05.2010)]
But what we really want to do, is loop over each post in that array and render a piece of html. That is as simple as it sounds:
// in a skin
<% for post in <% posts %> render 'postOverview' %>
<% subskin 'postOverview' %>
<h2><% post.title %></h2>
This will render the subskin 'postOverview' for each post in posts. 'postOverview' is a template we re-use in each iteration to render the current post. The way we render 'postOverview' in the for-loop, it has access to the context variable 'post' and uses it to output <% post.title %>
.
Subskins are an important concept in Ringo. A skin can have a lot of subskins, each of which starts with <% subskin 'foobar' %>
and ends where the next subskin starts.
The other use for subskins, besides loop rendering, is to overwrite a subskin of the same name defined in the skin we extend. index.html
extends from base.html
. In index.html
the 'content' will be the list of posts; therefor we overwrite the 'content' subskin with the loop we put together above.
This is how it looks all together:
// index.html
<% extends ./base.html %>
<!-- we overwrite the 'content' subskin which was
originally defined in base.html -->
<% subskin content %>
<% for post in <% posts %> render 'postOverview' %>
<!-- the 'postOverview' subskins is used by the for loop
to render each post -->
<% subskin 'postOverview' %>
<h2><% post.title %></h2>
<p>
<% post.createtime | dateFormat "dd.MM.yyyy" %>, <% post.author %>
</p>
<div>
<% post.text %>
</div>
Note how we use a filter to format the createtime: <% post.createtime | dateFormat "dd.MM.yyyy" %>
. We already have quite a lot of useful filters and macros in Ringo.
Start the app again http://127.0.0.1:8080.
$ ringo-web
The Skins Demo also shows of some skin features I did not mention. Also note that this is a very simple action. Later we will look at an action in the admin section of the Blog which creates Posts. It will have to deal with the Request object which is passed to every action as the first parameter req
.
FIXME how to write macros & filter. at least 1 simple example for both.
It would be nicer if the startpage of our blog would only show the lead text of every entry with a link to the actual blog post. For that to work we need to set a new lead
property on all our blog posts, and then define a new action to handle displaying a single post.
If you have read the part about querying above you will get this without further explanation: We just load the posts and add a property lead
to each and save.
>> include('model')
>> posts = Post.query().select()
[Post: Second Post (simon, 11.05.2010)],[Post: Introductory Post (simon, 11.05.2010)]
>> posts[0].lead = 'In which we describe the introduction'
In which we describe the introduction
>> posts[0].save()
>> posts[1].lead = 'In which we introduce the blog'
In which we introduce the blog
>> posts[1].save()
Then we modify index.html
to only render the lead and add a link to the full post. We use the href
macro to create the links. Use the href
macro to create relative links within your app.
Let's say every blog post should be reachable by a "read more" link (we will later write the action showing the full blog post):
// index.html
<% extends ./base.html %>
<% subskin content %>
<% for post in <% posts %> render 'post' %>
<% subskin 'post' %>
<h2><% post.title %></h2>
<p>
<% post.createtime | dateFormat "dd.MM.yyyy" %>, <% post.author %>
</p>
<div>
<p>
<% post.lead %>
</p>
<a href="<% href post %>/<% post._id %>"> ...read more </a>
</div>
Now we get links like "/post/1/", "post/2/". In the next sections we will take care of handling those so they actually output the full post.
Now the 'read more' href links to /post/id
where id
is the id of a post. We need a new action to handle the rendering of a single post. Remember how all the methods we export in actions.js
are already mapped to Urls. This is because currently in config.js
we just have that one line mapping:
// config.js
exports.urls = [
['/', './actions'],
];
Ringo will convert the pattern string '/'
into a Regex /\/.*/
, and that particular Regex will match any request path. Ringo then takes the first part of the path - everything up to the first '/' - and searches for a function with that name in the specified module actions
.
That simple mechanism worked great for us so far because by default - if there is no path part, because the Url was "/" - Ringo looks for an "index" action and that is all we needed.
But how will the URL /post/id
work? The first part is the action name: 'post'. Ringo will pass the rest of the URL - split by /
- as parameters to the action. The action just has to accept the correct number of arguments: in this case only one argument, the id.
So our post
action looks like this. It gets the post with the right id, as passed to it as the second argument, and renders that post. This time we use the get(id)
function of the entity to retrieve only the one post we care about. No need for query()
.
// actions.js
exports.post = function post(req, id) {
var post = model.Post.get(id);
return response.skinResponse('skins/post.html', {
post: post,
});
};
.. and the accompanying skins/post.html
. This skin is even simpler, as it doesn't do any looping. It only overwrites the 'content' subskin to output the post:
// skins/post.html
<% extends ./base.html %>
<% subskin content %>
<h1><% post.title %></h1>
<p>
<% post.createtime | dateFormat "dd.MM.yyyy" %>, <% post.author %>
</p>
<div>
<p><% post.lead %></p>
<p><% post.text %></p>
</div>
<a href="<% href / %>"> back to front </a>
Our app should be fully functional again, let's try it: http://127.0.0.1:8080.
$ ringo-web
Creating Posts in the Ringo shell was a great way to show you how our Storage API works. But in the real world you will have a backend to edit and create posts. That is what we will build now. Keeping it simple, we just add two actions: one to edit posts and another one to create them.
We want the URLs to look like this:
- /admin/edit/id show the form for editing Post with id
- /admin/create/ show an empty post form for creating new Posts
Some kind of authentication would be nice. Ringo ships with an authentication middleware which allows us to define protected URLs and users who can access. That will do for now. We will take a closer look at this middleware in a later section.
To show of Ringo's logging facility we will also log everything that goes on in the backend.
Our super simple URL mapping brought as far - with all the automatic parameter capturing & passing that is going on. Though for the admin backend we have to extend it. As I layed out above, all the backend actions will have the common prefix /admin/
. The easiest way to setup a mapping for this is to put all the admin actions in a separate file - adminactions.js for example - and map every Url that starts with /admin/
to that file. Easy:
// config.js
exports.urls = [
['/', './actions'],
['/admin/', './adminactions']
];
That's it already. For the Url /admin/create
to work we only need to add a create
action in adminactions.js
- will write that in the next section.
The edit
action has two tasks:
- it must output a form with the Blogpost's data for editing
- when the user clicks 'save' it must accept the incoming POST request and modify the Blogpost's data accordingly.
Ringo has a builtin mechanism for dispatching on the request method, which we will use here.
So far we have only seen actions that are plain functions. Those plain functions will be triggered for all HTTP methods (POST, GET, etc.). Instead of the action being a function, it can also be an object literal with properties matching the HTTP method names. For our edit
action we need one for GET and another for POST:
// adminactions.js
exports.edit = {}
exports.edit.GET = function edit(req, id) {
// output the model data for displaying
var post = model.Post.get(id);
return response.skinResponse('skins/edit.html', {
post: post,
});
};
exports.edit.POST = function edit(req, id) {
// TODO handle post data
return response.redirectResponse(req.path);
};
The POST action so far only redirects back to the GET action. Let's first deal with the edit.html
skin. It displays the form for the passed post
object:
// edit.html
<% extends ./base.html %>
<% subskin content %>
<h1> Edit Post '<% post.title %>' </h1>
<form name="blogpost" action="<% href %>" method="POST">
<h3>Title<h3>
<input type="text" name="title" size="30" value="<% post.title %>"><br/>
<h3>Lead<h3>
<textarea name="lead" cols="50" rows="5"><% post.lead %></textarea>
<h3>Text</h3>
<textarea name="text" cols="50" rows="20"><% post.text %></textarea>
<br/>
<input type="submit" name="Save" value="save"/>
</form>
This should already work! Try accessing http://127.0.0.1:8080/admin/edit/1. The form displays and you can press Save and that will redirect you back to the GET.edit
action.
Ringo passes the Request
object as the first argument req
to every action. We have seen this before, but in the edit
action we will finally do something with it. We loop over the list of editable properties, read them from req.params
and set them on the Blogpost.
// in an action
var post = model.Post.getById(id);
for each (var key in ['text', 'lead', 'title']) {
post[key] = req.params[key];
}
req.params
holds all GET as well as POST parameters. See Request for more info on the Request class.
Finally we save()
the modified post and redirect back to the GET.edit
action. This works because req.path
holds the current request path (/admin/edit/2
for example). The whole action is still quiet simple:
// adminactions.js
exports.edit.POST = function edit(req, id) {
var post = model.Post.getById(id);
for each (var key in ['text', 'lead', 'title']) {
post[key] = req.params[key];
}
post.save();
return response.redirectResponse(req.path);
};
The create
actions are even simpler. The GET.create
function renders the edit.html
skin like GET.edit
does but without passing a post, thereby creating an empty form. And that's it:
// adminactions.js
exports.create = {};
exports.create.GET = function create(req) {
return response.skinResponse('skins/edit.html');
};
The POST.create
stores what properties it gets via req.params
in a new Post
object but also attaches the automatically created author
and createtime
properties. And finally, it redirects to the GET.edit
action of the newly saved Post
:
// adminactions.js
exports.create.POST = function create(req) {
var post = new model.Post();
for each (var key in ['text', 'lead', 'title']) {
post[key] = req.params[key];
}
post.author = 'unknown author';
post.createtime = new Date();
post.save();
// once the Post is stored, redirect to it's edit page
return response.redirectResponse('./admin/edit/' + post._id);
};
Not bad. Startup your blog and try creating a new Post by opening http://127.0.0.1:8080/admin/create
Two things are annoying: both, the create and edit page, say 'Edit Post' at the top. A quick fix for this is that we extract the header as a subskin. We should only render the header subskin if the object post
is set in the skin context. A good opportunity to introduce the if
macro:
// edit.skin
<% extends ./base.html %>
<% subskin content %>
<% if <% post %> render editHeader %>
<form name="blogpost" action="<% href %>" method="POST">
<h3>Title<h3>
<input type="text" name="title" size="30" value="<% post.title %>"><br/>
<h3>Lead<h3>
<textarea name="lead" cols="50" rows="5"><% post.lead %></textarea>
<h3>Text</h3>
<textarea name="text" cols="50" rows="20"><% post.text %></textarea>
<br/>
<input type="submit" name="Save" value="save"/>
</form>
<% subskin editHeader %>
<h1> Edit Post '<% post.title %>' </h1>
This will do for now.
Also it would be nice to have messages like "Post saved successfully" after saving. One way to do this is use the req.session
(see ringo/webapp/request.Session). We can set a message
property on req.session
in POST.edit
and read that property from the session in GET.edit
.
Note how we also unset the req.session.message
in Get.edit
- we do no want it lurking in the session forever:
// actions.js
exports.edit = {}
exports.edit.GET = function edit(req, id) {
// ...
var message = req.session.data.message;
req.session.data.message = "";
return response.skinResponse('skins/edit.html', {
post: post,
message: message,
});
};
exports.edit.POST = function edit(req, id) {
// ...
post.save();
req.session.data.message = "Successfully saved Post " + id;
return response.redirectResponse(req.path);
};
Adding <% message %>
to the edit.html
skin is left as an exercise.
Manually hacking the Url to get into the admin interface is cumbersome. Let's build a simple admin dashboard, reachable under /admin/ that lists all stories with an edit link. This is pretty straight forward so I won't explain much. First the index
action, which just renders skins/adminindex.html
with all posts in the context:
// adminactions.js
exports.index = function index(req) {
var posts = model.Post.query().select();
return response.skinResponse('skins/adminindex.html', {
posts: posts,
});
};
.. and the accompanying skin adminindex.html
, which renders all posts, links to their edit page and has a "create new post" link on top:
// adminindex.html
<% extends ./base.html %>
<% subskin content %>
<h1> Demoblog Admin Interface </h1>
<a href="./create"> new post </a>
<ul>
<% for post in <% posts %> render 'post' %>
</ul>
<% subskin 'post' %>
<li>
<a href="./edit/<%post._id%>"><% post.title %></a>
<% post.createtime | dateFormat "dd.MM.yyyy" %>, <% post.author %>
</li>
Tada: http://127.0.0.1:8080/admin/
For authentication we use Ringo's basic auth middlware. Like any middleware it is activated by adding it to the array middleware
in config.js
:
// config.js
exports.middleware = [
'ringo/middleware/responselog',
'ringo/middleware/error',
'ringo/middleware/notfound',
'ringo/middleware/basicauth'
];
But that has no visible effect unless we also define a protected realm. In our case all backend Urls start with '/admin/' so that will be the realm, which only the user 'demoblog' with the password 'secret' can access. The passwords is given as a SHA1 hash. The final auth config looks like this:
// config.js
exports.auth = {
'/admin/': {
blogadmin: "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4" // "secret"
}
};
You can create the SHA1 for a string with Ringo's ringo/utils/strings
digest()
method:
>> var strings = require('ringo/utils/string')
>> strings.digest('secret', 'sha1')
e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4
When you access any of the /admin/ Urls you should now get a basicauth prompt, try http://127.0.0.1:8080/admin/.
FIXME: explain middleware in general and auth in particular
Ringo come with a logging Module. Instantiating a logger is easy, put this somewhere at the top of adminactions.js
:
// adminactions.js
var log = require('ringo/logging').getLogger(module.id);
Then in the edit.POST
and create.POST
actions we log which user updated what post. With authbasic the user is sent as in the header of every request, we will need a utility function in adminactions.js
to extract the current user from the request. I copied this from ringo/middleware/auth
:
// adminactions.js
function authUser(req) {
var credentials = base64.decode(req.headers.authorization
.replace(/Basic /, '')).split(':');
return credentials.length && credentials[0];
}
Now we can start logging. And while we are at it we can finally fix create.POST
to actually set the correct author. Note how we use {}
(curly bracket pairs) for string replacement. The logger will replace the bracket pairs with the following arguments.
// adminactions.js
exports.create.POST = function create(req) {
var post = new model.Post();
for each (var key in ['text', 'lead', 'title']) {
post[key] = req.params[key];
}
var user = getAuthUser(req);
post.author = user;
post.createtime = new Date();
post.save();
log.info('{} created by {}', post, user);
return response.redirectResponse('./edit/' + post._id);
};
This will yield a log line like this:
8068446 [qtp1868018799-13] INFO adminactions - [Post: Second Post (simon, 04.08.2010)] updated by blogadmin
... to be continued http://gist.github.com/gists/399012
Perhaps you're missing require('ringo/webapp/util');? Otherwise get() does not work. But I may have misunderstood something.
Jirka