Tutorial
This tutorial only works with RingoJs 0.7
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.
Sources:
Help
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.Content
Environment
Storage
- Configuring a Store
- Schemafull DBs - What about MySQL, PostgreSQL, etc.?
- Create Queries
- Select Queries
- Ringo comes with Batteries included
- Update queries
Actions & Skins
URL Mapping
- URL Patterns: Capturing Arguments for Actions
- Roadmap for the Rest of the Tutorial
- Url Pattern: mapping a module to Urls
- RESTful Method Dispatch
More on Actions & Skins
Miscellaneous
- Middleware
- Logging
- Debugger
- App Engine Deployment
- Scripting Java
About
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.Sources:
Help
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.Webapp Scaffolding
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.jsThe script bootstrapping your web application.config.jsSettings for middleware, database, the app itself, etc.actions.jsFunctions creating the Response. The View in MVC.
.. and several directories with rather self explanatory names:
skins/Put all your skins (templates) in here.public/Ringo servers files in this directory as static content.config/Holds webserver configuration files. Do not worry about those.
Calling ringo main.js will start a development webserver, jetty, on your machine and you can view the demoblog app right away in your browser.
$ cd demoblog
$ ringo main.js
This will start the server on http://127.0.0.1:8080.
Under Linux 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:
$ 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 adding/modifing the httpConfig.port property:
// config.js
// NOTE: if you do copy&paste, please remove comments.
// Otherwise some examples will not work, in particular skins.
exports.httpConfig = {
port: '8787',
};
JavaScript Modules
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.
- Any function or other property, that you attach to
exportsin your module, should be exposed. 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 useincludein 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.
Configuring a 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. For bigger apps you might want to consider a model/ directory where each model gets its own module. For this tutorial a single model.js will do. First we import the filestore model and instantiate a store on which we operate:
// model.js
var filestore = require('ringo/storage/filestore');
var storePath = './db';
var store = new filestore.Store(storePath);
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.
// model.js
var filestore = require('ringo/storage/filestore');
var storePath = './db';
var store = new filestore.Store(storePath);
exports.Post = store.defineEntity('Post');
exports.Comment = 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. Those constructors returned by defineEntity also have functions for querying the models - we will look at them later.
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.
Schemafull DBs - What about MySQL, PostgreSQL, etc.?
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 you're using something like MySQL your model definition would 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},
}
});
exports.Post = Post;
.. and you would have to instantiate a different store in config.js - you can currently choose between ringo-sqlstore (written in javascript using JDBC) and ringo-hibernate (which is built on top of http://hibernate.org/).
You can install those with ringo-admin. See How to install packages if you need more help with that.
$ ringo-admin install robi42/ringo-hibernate
$ ringo-admin install grob/ringo-sqlstore
Both of them are works in progress and do not have much documentation, though they do comply to the evolving Ringo store interface.
ringo-sqlstore's readme gives you a starting point; and ringo-hiberante's unit tests will help you understand how it is different and what it's capable of.
There are in fact other store implementation, check out the Database section in the list of available packages for Ringo.
Create Queries
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 secondPost = 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!
Select Queries
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.
Ringo comes with Batteries included
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');
exports.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.
The global object holds a couple of useful global functions besides require and import we also have utilites to interface with Java.
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.
Update queries
Now we can more easily tell which Entity needs fixing. Before we query for the post we should 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!
Actions
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.
// actions.js
var {Response = require('ringo/webapp/response');
var model = require('./model');
exports.index = function(req) {
var posts = model.Post.query().select().slice(0,10);
return Response.skin('skins/index.html', {
posts: posts,
});
};
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);
We also import ringo/webapp/response to create the actual Response object the action returns. Response has various static helper functions to, for example, quickly return a rendered skin or do a redirect. See the API docs on Response for a complete list.
In this case we use Response.skin(skin, context) to return the rendered html to the browser. Response.skin(skin, context) 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. The skin can now access the "posts" array from the context and we will write the necessary view logic to render each post.
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 argument req.
See the next section for what happens in the skin.
Skins and Subskins
Since we put the 'posts' array in our context, we could just put<% posts %> in the skin and it would output:
// rendered skin output of <% posts %>
[Post: Second Post (simon, 11.05.2010)], [Post: Introductory Post (simon, 11.05.2010)]
.. the same string representation we would get with print(posts) on the shell. 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 'postOverview' subskin has access to the new 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
// Please remove comments.
<% 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>
There is another new concept in this skin: a filter. The filter dateFormat is used to format the date object into something more human readable. More about filters and macros in the next section.
Macros & Filters
We already have quite a lot of useful filters and macros in Ringo.
The Skins Demo also shows of some skin features I did not mention.
FIXME- were to configure macros (config.js array). they just something that is part of the skin context.
- how to write macros & filter. at least 1 simple example for both.
- rip of old helma tutorials
- http://helma.org/wiki/New+Skin+Features+in+Helma+1.6/
- http://helma.org/Documentation/Request-Response-Cycle/#MacroHandlers
Refining our Skins
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 newlead 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.
URL Patterns: Capturing Arguments for Actions
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 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 / (slash), as parameters to the action. The action must accept the correct number of arguments: in this case only one argument, the id.
The post action below works 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(req, id) {
var post = model.Post.get(id);
return Response.skin('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 start the server ...
$ ringo main.js
... and try it: http://127.0.0.1:8080.
Roadmap for the Rest of the Tutorial
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.
Url Pattern: mapping a module to Urls
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 module. Create adminactions.js to hold our adminactions module and map every Url that starts with /admin/ to that module:
// 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.
FIXME: module actions/admin is saner than adminactions
RESTful Method Dispatch
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
var {Response} = require('ringo/webapp/response');
var model = require('./model');
exports.edit = {};
exports.edit.GET = function(req, id) {
// output the model data for displaying
var post = model.Post.get(id);
return Response.skin('skins/edit.html', {
post: post,
});
};
exports.edit.POST = function (req, id) {
// TODO handle post data
return Response.redirect(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>
// href macro without parameters links to _this_ document.
<form name="blogpost" action="<% href %>" method="POST">
<h3>Title<h3>
<input type="text" name="title" size="30" value="<% post.title %>">
<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 edit.GET action.
The Request Object
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.get(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 edit.GET 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 (req, id) {
var post = model.Post.get(id);
for each (var key in ['text', 'lead', 'title']) {
post[key] = req.params[key];
}
post.save();
return Response.redirect(req.path);
};
The create actions are even simpler. The create.GET function renders the edit.html skin like edit.GET does but without passing a post, thereby creating an empty form. And that's it:
// adminactions.js
exports.create = {};
exports.create.GET = function(req) {
return Response.skin('skins/edit.html');
};
The create.POST 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 edit.GET action of the newly saved Post:
// adminactions.js
exports.create.POST = function(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.redirect('/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
Skins: if macro
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 objectpost 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.
Sessions
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 edit.POST and read that property from the session in edit.GET.
Note how we also unset the req.session.message in edit.GET - we do no want it lurking in the session forever:
// adminactions.js
exports.edit.GET = function(req, id) {
// ...
var message = req.session.data.message;
req.session.data.message = "";
return Response.skin('skins/edit.html', {
post: post,
message: message,
});
};
exports.edit.POST = function(req, id) {
// ...
post.save();
req.session.data.message = "Successfully saved Post " + id;
return Response.redirect(req.path);
};
Adding <% message %> to the edit.html skin is left as an exercise.
Building the Admin Dashboard
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 theindex action, which just renders skins/adminindex.html with all posts in the context:
// adminactions.js
exports.index = function(req) {
var posts = model.Post.query().select();
return Response.skin('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/
Middleware
For authentication we use Ringo's basic auth middlware. Like any middleware it is activated by adding it to the arraymiddleware in config.js:
// config.js
exports.middleware = [
require('ringo/middleware/gzip').middleware,
require('ringo/middleware/etag').middleware,
require('ringo/middleware/static').middleware(module.resolve('public')),
// require('ringo/middleware/responselog').middleware,
... cut for clearity ...
];
By convention every middleware module - like ringo/midleware/basicauth - exports a function named middleware which acts as the actual middleware function. BasicAuth is a special middleware as it needs a configuration object. Similar to the Static-Middleware which accepts the path it should serve statically.
We have to setup an auth config object to pass to the middleware. In our case all backend Urls start with '/admin/' so that will be the realm, which only the user 'blogadmin' with the password 'secret' can access. The passwords is given as a SHA1 hash. The final auth config looks like this:
// config.js
var authConfig = {
'/admin/': {
blogadmin: "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4" // "secret"
}
};
Which we will put into action by passing it to the BasicAuth-middleware:
// config.js
exports.middleware = [
require('ringo/middleware/gzip').middleware,
require('ringo/middleware/etag').middleware,
require('ringo/middleware/static').middleware(module.resolve('public')),
//require('ringo/middleware/responselog').middleware,
require('ringo/middleware/error').middleware,
require('ringo/middleware/notfound').middleware,
require('ringo/middleware/basicauth').middleware(authConfig),
];
You can create the SHA1 for a string with Ringo's ringo/utils/strings digest() method:
>> var strings = require('ringo/utils/strings')
>> 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, how to write one
- what does
middlewarefunction do -> it's a JSGI app
Logging
Ringo come with a logging Module. Instantiating a logger is easy, put this somewhere at the top ofadminactions.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.
Because of the authbasic middleware we installed earlier the username must be in the header of every request. A small utility function in adminactions.js extracts the current user from the request. I copied this from ringo/middleware/auth:
// adminactions.js
function getAuthUser(req) {
var credentials = require('ringo/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(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.redirect('./edit/' + post._id);
};
Now, whenever you create a new post it will yield a log line like this to console:
8068446 [qtp1868018799-13] INFO adminactions - [Post: Second Post (blogadmin, 04.08.2010)] created by blogadmin
This tutorial can also be found on git
