Writing your first Wilson App, part 1

Welcome to Wilson! To get started, let’s take a look at what’s involved in setting up a basic blogging application. This assumes you already have Wilson installed. You can tell that Wilson is installed by running node and typing require('wilson'). If that command returns a large object literal, then you’re in the clear.

Creating a project

Wilson is designed from the bottom up to feel like writing Django code. It borrows heavily from Django, not least of all in it’s concept of “projects” and “applications”. To start, cd into a directory where you’d like to start your Wilson project, and run wilson core:startproject mysite. This will create a mysite directory in your current directory.

It should contain:

mysite/
    index.js
    urls.js
    settings.js
    manage

These files are:

  • index.js: Declares this directory as a module (similar to __init__.py in Python).
  • urls.js: A stub for the base routes for your project.
  • settings.js: Base settings for your project.
  • manage: A shortcut to the wilson command that automatically uses the local settings.js.

Running your project

Now that you’ve created a base project, it might be worthwhile to check that it worked. Run mysite/manage core:runserver 8000 to run your project as a Node.js server.

When you open the http://localhost:8000 in your browser, you should see a familiar welcome page (NOT IMPLEMENTED YET).

Create an app

Run mysite/manage core:startapp blog. This should create a directory called blog containing:

blog/
    views.js
    urls.js
    index.js
    models.js

These files are:

  • views.js: A file containing your view functions.
  • urls.js: A file defining url patterns for your app.
  • models.js: A file containing model definitions.
  • index.js: The main file of the application – this defines the linkage between your app and external applications, as well describing default settings for your app.

Now hold on a minute before we get too far into this – Wilson clearly borrows the idea of apps from Django. Why would we need an index.js file? In Django, your models are autodiscovered once you add that app into the list of INSTALLED_APPS, and you can easily hook up urls by using include and pointing them at a urls file that exists on your PYTHONPATH. Isn’t it a little boilerplate-y to have to manually describe your app to the outside world?

Application, and Application Instances

In short, no. Wilson puts control over the apps you leverage in your site firmly back in your hands. This means that apps must provide your project with the following:

  • provides: A one-word description of what this app provides to other apps – for instance, “auth”.
  • models: An object literal of models that the application provides – mapping name to model.
  • external_apps: An object literal of external_app_label to finding function. For instance, you can select the primary application that provides auth by saying myauth:primary('auth').
  • urls: If your app provides URLs, they should be supplied here.
  • template_directories: if your app has template directories, they can be added here and will be automatically added to your template library’s path.
  • settings: An object literal of settings values for your app. These can be overridden in a project’s settings.js, but provide useful defaults.
  • middleware: An object literal mapping middleware_class_name to middleware_object.

Wilson requires application authors to provide this information because, until an app is instantiated – that is, included in the object literal INSTALLED_APPS, it has no meaning. Models cannot be imported from a base app, since Wilson does not know how you intend to reference it.

Your view functions will usually be called as a method on the current application instance. That means that usable models are accessible as follows:

// for local models:
var Entry = this.models.Entry;

// for external dependencies
var User = this.externals.auth.models.User;

The main result is that you can safely install one application many times, and override it’s external dependencies within your project’s settings. Which is pretty cool.

Sorry for the tangent

Getting back to it, let’s install your application into your project.

In settings.js, add:

'myblog':app('blog'),

to:

INSTALLED_APPS = {
    'core':apps.usePrimary('wilson/core'),
    'sessions':apps.usePrimary('wilson/contrib/sessions'),
    'auth':apps.usePrimary('wilson/contrib/auth'),
    'myblog':apps.use('blog'),       // <--- our newly install blog!
};

We’ve just added an instance of your blog app to this project’s installed apps. The apps.use(...) portion of this plainly includes the blog app as a normal app with no special attributes. If you use apps.usePrimary(...) this app instance will be marked as the primary instance the app. For now, that doesn’t matter too much, but it comes in handy when defining your application’s external dependencies.

And in your urls.js, add:

exports.patterns = routes(''
    , app('^/myblog/', 'myblog')
)

This usage of app(...) denotes that we should include the url patterns from the app instance named “myblog”, and all views within should execute in that app instance’s context.

And now your app is installed – though it won’t do much good as is, since there are no views! Open up blog/urls.js and it should look like this:

var escaperoute = require('escaperoute'),
    routes = escaperoute.routes,
    url = escaperoute.url,
    surl = escaperoute.surl;

exports.patterns = routes('blog/views'
    //, url('list/$', 'list_view', 'blog-list-view')
);

Edit the file to add a view:

exports.patterns = routes('blog/views'
    , url('list/$', 'list_view', 'list_view')
    , surl('([:w:d\\-_]+)/$', 'detail_view', 'detail_view')
);

The first argument to routes defines where these views should be imported from (a la Django). The next two arguments define routes to named views. surl and url are fairly interchangeable – surl allows you to write regexen using : as the special character instead of \. The first argument is the regex to match, the second is either a string name of a function to be imported, or an actual callable. The last argument is the name of the view for reverse matching purposes.

And open up views.js, and edit it to look like so:

exports.detail_view = function(request, slug) {
    getObjectOr404(request)(this.models.Entry, {'slug':slug}, function(entry) {
        renderToResponse(request)('blog/entry_detail.html', {
            'entry':entry
        });
    });
};

exports.list_view = function(request) {
    this.models.Entry.objects.filter({'public':true}).all(function(objects, error) {
        if(error) {
            // this will trigger the error handling middleware
            request.attemptContinue([error]);
        } else {
            renderToResponse(request)('blog/entry_list.html', {
                'entry_list':objects
            });
        }
    });
};

This is slightly more complex than a Django views.py. As you can see, arguments from the routes are appended to the view function when it is called – hence, detail_view‘s slug will contain the match from the url.

getObjectOr404 takes a single argument – request, and returns a function that takes three arguments – resource, filter, and callback. If no matching object is found the request is immediately ended with a 404. Otherwise it is passed as the first argument to the callback function, where we take it and render it to a template.

renderToResponse works similarly to getObjectOr404 in that it takes a request and returns a function. This function’s arguments are template_name and context_dict.

In list view, we query the Entry resource for all public entries, and provide it with a callback that takes object_list and error. If there is an error, we pass it to the request and ask the request to attemptContinue.When objects are passed into request.attemptContinue, they are treated as errors and will trigger the processException middleware.

Defining our models

Just like the divide between Application and ApplicationInstance, models are divided into Models and Resources. Models are the platonic ideal of Resources – they define how a Resource should be created. To create a resource, they need the context of an ApplicationInstance. To be more explicit: Model‘s cannot be queried, filtered, or used in any way, until they are transformed into Resource‘s. When operating within the context of an application instance, your resources are available to you via this.models.<ModelName> and this.externals.<external_name>.models.<ModelName> – these can be used to create, read, update, and delete actual database rows.

Let’s define our Entry model in models.js:

var reverse = wilson.urls.reverse;

exports.Entry = models.model({
    'public':models.BooleanField(),
    'title':models.CharField({max_length:255}),
    'tease':models.CharField({max_length:255}),
    'slug':models.CharField({max_length:40, unique:true}),
    'body':models.TextField(),
    'published':models.DateTimeField({default:function() { return new Date(); }}),
    'author':models.ForeignKey(models.dep('ourauth', 'User'), {'related_name':'entry_set'}),
    'toString':function() {
        return this.title;
    },
    'getAbsoluteURL':function() {
        return reverse([this._meta.app_name, 'detail_view'].join(':'), [
            this.slug
        ]);
    }
    Meta:{
        'ordering':'-created'
    }
});

This should look somewhat familiar if you’re coming from Django. Kwargs are passed into the fields using object literals, the Meta class is present, and any function attached to the model – like toString – will always be executed in the context of the model instance.

The author ForeignKey takes a model or dependency for its first argument. In this case, we depend upon an external app named ourauth, and we want to use the User model from that app instance.

We need to add ourauth as an external in our app.js file:

'external_apps':{
    'ourauth':primary('auth'),
}

Should do the trick. This says that we want the primary app instance that provides the tag auth, and we want to use it as ourauth.

Creating our database

Run createdb mysite, and then add the following to your settings.js file:

var pieshop = require('pieshop');

pieshop.settings.set_addon('backend', 'postpie.backends:PostgresBackend');
pieshop.settings.set_addon('transport', 'postpie.transports:PostgresTransport');

pieshop.settings.set_value('DB_HOST', 'localhost');
pieshop.settings.set_value('DB_NAME', 'mysite');

We’re stating that we’re using the local database named mysite, and that we want to use the PostgresBackend as our backend and PostgresTransport as our transport. Now we can run:

./manage core:syncdb | psql mysite

To sync our database.