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 "<a href=\"" .. self.url .. "\">" .. author .. "</a>"
elseif self.email and self.email ~= "" then
return "<a href=\"mailto:" .. self.email .. "\">" .. author .. "</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>
|