The controller is the the part of Nitro which provides the general logic of how your application functions. So: more background logic, before we can see anything at all.
Have a look at the run.rb again, and you can see 2 controllers there, the
MainController and the MailController.
Let's start with an easy example, which is why I introduced the
MainController in the first place.
# Class MailController
class MailController < Nitro::Controller
def index
print "<a href='/mail'>List mails</a>"
end
end
This is, how every Nitro controller is built. The controller is a class derived from
Nitro::Controller. Every method, like index (which is a special case, it
points to the root: /), maps to an URL.
Let's map out our Nitro application:
MainController (mount point: '/')
def index => '/'MailController (mount point: '/mail')
def index => '/mail'def view(msg_id) => '/mail/view'def search => '/mail/search'Now, that was real easy, wasn't it?
The mail controller is just as easy. This will also be the part where we make use of some nifty Og features.
Let's go through the code line by line, because this one is important.
3 helper :pager
This provides a handy feature, the paginate() function, with which you can easily distribute huge data amounts over several pages.
def indexThis function gets called, when you browse to http://localhost:9999/mail.
6 @entries, @pager = paginate(IndexedMail, :per_page => 10, :order => 'date DESC')
This line creates an array called @entries, and a @pager object. We will
use the @pager object later, in our templates, to get the number of the pages and 'forward'
and 'backward' links to browse the Mails.
def view(oid)When you browse to http://localhost:9999/mail/view/5, or any other number
after view/ which is a valid oid in the database, you will get a single
mail to read.
As you can see, the method view accepts a single parameter, for example
you would not be able to call /mail/view/4/asdf. It would raise an
ArgumentError: wrong number of arguments.
So, for each parameter, you can get a value. If you want an arbitrary number
of arguments, use def function(*args), like elsewhere in ordinary Ruby code.
12 flash[:err] = 'No such Email' 13 redirect_to 'index'
The 'flash' is a special object, which provides a way to pass information to the next rendered page. This is particularily useful for error messages, which you want to keep even after doing a redirect to another page.
The redirect_to 'index' redirects to /mail/index but you could also create
a special error page to handle the error more explicitly.
After the check if the oid is an actual number greater than zero, we fetch the
IndexedMail object we want to display.
16 @mail = IndexedMail[oid]
This stores the oid'th Mail in the variable @mail.
You might start seeing the pattern here, everything I want to pass to the View later is a instance variable. In the chapter about Views you will see that you can just use those instance variables as if you were directly in that Controller class.
As you can see, the view function does nothing else, apart from some error
handling, than getting an IndexedMail object and storing it in an instance
variable.
def searchAbove you have seen that you can call controller methods via the
browser's URL. Now, why doesn't the search method have any parameters?
The reason is simple: I want to not only catch GET requests, I want to get
POSTs as well. By using the request facility you can get the parameters you want.
If you want to make sure, or just want to check which request method is
used, you can use request.post? or request.get? or just request.method.
26 if @query = request['q'] || request['search'] || request['query']
This line means: 'if there is a ?q=data, a ?search=data or a ?query=data
appended to your GET request or in you POST body, it will use it as the
@query.
Example: http://localhost:9999/mail/search?query=chunky%20bacon
27 @query.gsub!(/\b\s+\b/, '|') # spaces between words
Well, well, here we got some regex fun again :D. This is a convenience
workaround to be able to use more intuitive queries for searching.
About search queries:
Parses a query, which should be single words separated by the boolean operators "&" and, "|" or, and "!" not, which can be grouped using parenthesis.
That means there must always be a symbol between two words. With the regex
I just work around that and treat whitespace between two words as a OR.
Boring, yeah, next up: something very neat :D
29 sql_query = "SELECT * FROM findogindexedmail('#{Og.escape(@query)}') ORDER BY rank LIMIT 5"
HAH! See why we built some neat database first? Now we actually call our
findogindexedmail('query') function from before. Og.escape is something
you should remember and make use of, if you want pass any data from users (evil!)
to the database. Always take some precaution against injection! Who knows where that needle has been ;/.
31 @results = IndexedMail.find(:sql => sql_query, :select => '*')
Here we search for results and put them into a @results array. It executes
the query we created before. If you already used Og a bit before, you may
know about the :sql option, which lets you execute arbitrary SQL. But you most
probably don't use or know the :select option. It gets fields
which are not part of the Model we defined before. We use the :select to
get the rank field as well as the others.
Now we have everything together, we have search results at our fingertips, we just have to provide some interface to them, something human-viewable anyway (are programmers humans...?).
1 # Class MailController 2 class MailController < Nitro::Controller 3 helper :pager 4 5 def index 6 @entries, @pager = paginate(IndexedMail, :per_page => 10, :order => 'date DESC') 7 end 8 9 def view(oid = nil) 10 if (!oid || oid.to_i <= 0) 11 flash[:err] = 'No such Email' 12 redirect_to 'index' 13 end 14 15 @mail = IndexedMail[oid] 16 17 unless @mail 18 flash[:err] = 'No such Email' 19 redirect_to 'index' 20 end 21 22 end 23 24 def search 25 if @query = request['q'] || request['search'] || request['query'] 26 @query.gsub!(/\b\s+\b/, '|') # spaces between words 27 28 sql_query = "SELECT * FROM findogindexedmail('#{Og.escape(@query)}') ORDER BY rank LIMIT 5" 29 30 @results = IndexedMail.find(:sql => sql_query, :select => '*') 31 32 end 33 34 end 35 36 end # end class MailController
Heh, that was quite some stuff, I hope I haven't scared you away by now. But this also shows the one of my most beloved Nitro features: It doesn't get in my way while I code. I just define some functions in a Ruby class, write workarounds, mess with anything I like. Nitro mostly just gives me more handy tools to work with, and if I don't like the proposed tools, I just write my own.
That actually wraps up the need for groundwork, we now have all that data lying around, crying to be used.
You remember the phrase, that the controller does all the heavy lifting? Ok
I lied. Heavy lifting does the poor Database System T_T You scare little
kids too, right? ;/
Basically we have 2 options, if you have PostgreSQL (good) you're lucky, if
not (bad), you'll either have to search for regex (yay!) functions yourself
or take my little query string builder using the LIKE keyword in standard
SQL.
25 def search 26 if @query = request['q'] || request['search'] || request['query'] 27 @query.gsub!(/[^\w]+/, '|') # replace everything wich is not a word char 28 29 sql_query = "body ~* '(#{@query})' OR header ~* '(#{@query})'" 30 31 @results = IndexedMail.find(:condition => sql_query) 32 33 end 34 end
This was the version with the nice regex system from PostgreSQL, look at the
~*, ain't it nice :D.
Another version would be:
25 def search 26 if @query = request['q'] || request['search'] || request['query'] 27 @query = @query.split(/[^\w]+/) # split at any non word character 28 29 sql_query = @query.map {|x| "body LIKE '%#{x}%'" }.join(" OR ") + ' OR ' + 30 @query.map {|x| "header LIKE '%#{x}%'" }.join(" OR ") 31 32 @results = IndexedMail.find(:condition => sql_query) 33 34 end 35 end
I think you can understand now, while I'm really all for standards, that sometimes convenience functions and or some special handling can lead to cleaner code in the frontend.
If you think a little, you can even make your own ranking system (count the occurance of the query words in the resulting mails perhaps?) and sort them. This is left as an exercise to the reader.