File: blog_doc.txt

package info (click to toggle)
lua-orbit 2.1.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 1,908 kB
  • ctags: 543
  • sloc: sql: 56; sh: 32; makefile: 28; xml: 20
file content (212 lines) | stat: -rw-r--r-- 8,101 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
This page documents the Blog example in the `samples/` folder of 
[[Orbit]]'s SVN repo. It is in the "literate programming" style, with code and
explantion insterspersed. The Blog example exercises most of the current
features of Orbit, and can be easily changed to suit the needs of
similar dynamic web sites.

## Initialization

First let's require Orbit itself and the other libraries that the Blog uses (the
SQLite3 LuaSQL driver and the Markdown parser). After that we declare
that the Blog is a Lua module, and an Orbit application by passing the
`orbit.app` option to `module`. Finally we load the application's configuration
data.

<pre>
require "orbit"
require "luasql.sqlite3"
require "markdown"

module("blog", package.seeall, orbit.app)

require "blog_config"
</pre>

`orbit.app` injects quite a lot of stuff in the `blog` module's namespace.
The most important of these are the `add_models`, `add_controllers` and
`add_views` methods that let you define the main functionality of the
application. It also defines a `mapper` variable that Orbit uses to create
the models (Orbit initializes this variable to its default ORM mapper). Finally,
it defines default controllers for 404 and 500 HTTP error codes as the
`not_found` and `server_error` variables, respectively. Override those if you
want custom pages for your application.

<pre>
local env = luasql[database.driver]()
mapper.conn = env:connect(database.conn_string)
</pre>

The code above initializes the DB connection for Orbit's default mapper. You need
to do this before creating the models because Orbit's default mapper hits the
database on model creation to fetch the DB metadata.

Now we are going to define the model part of the application. We do this by
calling `add_models`, passing a table with the models we want to create.
Orbit calls the mapper's `new` method for each model, passing the model
name and the table with your model's methods.

<pre>
blog:add_models{
</pre>

The first model we define is the `post` model. The default mapper will try to find
posts in a table called `blog_post` in the database. The `id` column is assumed
to be the primary key of the table.

<pre>
  post = {
    find_comments = function (self)
      return models.comment:find_all_by_post_id{ self.id }
    end,
    find_recent = function (self)
      return self:find_all("published_at not null",
                           { order = "published_at desc",
                             count = recent_count })
    end,
    find_by_month_and_year = function (self, month, year)
      local s = os.time({ year = year, month = month, day = 1 })
      local e = os.time({ year = year + math.floor(month / 12),
                          month = (month % 12) + 1,
                          day = 1 })
      return self:find_all("published_at >= ? and published_at < ?",
                               { s, e, order = "published_at desc" })
    end,
    find_months = function (self)
      local months = {}
      local previous_month = {}
      local posts = self:find_all({ order = "published_at desc" })
      for _, post in ipairs(posts) do
        local date = os.date("*t", post.published_at)
        if previous_month.month ~= date.month or
           previous_month.year ~= date.year then
          previous_month = { month = date.month, year = date.year }
          months[#months + 1] = previous_month
        end
      end
      return months
    end
  },
</pre>

There is no distinction between "class methods" and "instance methods" for models.
You define both of them inside the model table, and it is your responsibility to
not mix them up when you use your models. But this shouldn't be a surprise to Lua
users. In the case of the `post` model, all of the methods are "class methods", more
specifically finders. The default mapper defines a few generic finder methods, and
also creates tailored finders (such as `find_all_by_post_id` used in `find_comments`
on demand. Their use above should be self-explanatory.

The next model we declare is the `comment` model. It is much simpler,
with no custom finders, but it does have an "instance method" that
we use later in the view part of the application.

<pre>
  comment = {
    make_link = function (self)
      local author = self.author or anonymous_author
      if self.url and self.url ~= "" then
        return "&lt;a href=\"" .. self.url .. "\"&gt;" .. author .. "&lt;/a&gt;"
      elseif self.email and self.email ~= "" then
        return "&lt;a href=\"mailto:" .. self.email .. "\"&gt;" .. author .. "&lt;/a"
      else
        return author
      end
    end
  },
</pre>

Finally the `page` model just needs the default functionality, so we just
declare it as an empty table.

<pre>
  page = {}
}
</pre>

Now we are going to define the controllers of the application. In Orbit, each
controller has a list of patterns that Orbit matches against the `PATH_INFO`
to find the correct controller, and http methods that this controller handlers.
Each method receives the running application instance, and any captures
by the pattern.

<pre>
blog:add_controllers{
</pre>

The `index` controller shows all recent posts, and is pretty straightforward. All
GET requests to `/` or `/index` will go to this controller. It just fetches the
required model data from the database, then passes control to the `index`
view along with the model data.

<pre>
  index = { "/", "/index",
    get = function(self)
      local posts = models.post:find_recent()
      local months = models.post:find_months()
      local pages = models.page:find_all()
      self:render("index", { posts = posts, months = months,
                    recent = posts, pages = pages })
    end
  },
</pre>

The `post` controller shows a single post (and its comments). Any GET requests
to `/post/{post_id}` go to it. It is pretty similar to `index`, as most of the model
data that it has to load is the same (to render the nav bar, menu, and archive links).
Notice how `post` delegates to `not_found` when the post does not exist.

<pre>
  post = { "/post/(%d+)",
    get = function (self, post_id)
      local post = models.post:find(tonumber(post_id))
      local recent = models.post:find_recent()
      local pages = models.page:find_all()
      if post then
        post.comments = post:find_comments()
        local months = models.post:find_months()
        self:render("post", { post = post, months = months,
                     recent = recent, pages = pages })
      else
        self.not_found.get(self)
      end
    end
  },
</pre>

The `add_comment` model is the biggest, and most complicated, as it has
to handle POST methods. It also does some validation on the input. If the
comment field is empty it delegates back to the `post` controller, along with
a flag that will make the view display the appropriate error message. If not it
creates a new comment model object, fills it up and then writes it to the database.
The comment's `created_at` field is automatically set to the current time by
Orbit's model mapper. The controller also updates the comment count in
the post object. Finally, it redirects to the post page. The redirect avoids double
posting in case the user hits reload.

<pre>
  add_comment = { "/post/(%d+)/addcomment",
    post = function (self, post_id)
      if string.find(self.input.comment, "^%s*$") then
        controllers.post.get(self, post_id, true)
      else
        local comment = models.comment:new()
        comment.post_id = tonumber(post_id)
        comment.body = markdown(self.input.comment)
        if not string.find(self.input.author, "^%s*$") then
          comment.author = self.input.author
        end
        if not string.find(self.input.email, "^%s*$") then
          comment.email = self.input.email
        end
        if not string.find(self.input.url, "^%s*$") then
          comment.url = self.input.url
        end
        comment:save()
        local post = models.post:find(tonumber(post_id))
        post.n_comments = (post.n_comments or 0) + 1
        post:save()
        self:redirect("/post/" .. post_id)
      end
    end
  },
</pre>