Chapter 11 of the tutorial can be used pretty much as-is, but I found that after an error, the redirection specified by the error method would be overwritten by the helper aspect. After some research, I discovered that the latest version of Ramaze supports a redirection status operator, redirected?. Using this, the helper aspect can be written very simply:
helper :aspectChapter 12 of the tutorial needs no changes, except that you don't have to add the flash section to the page template because we did it earlier.
after( :create, :delete, :open, :close ) {
redirect Rs() unless redirected?
}
Now I wanted to convert the application to something more suited to enterprise deployment (and also more suited to shared hosting deployment - typically such environments provide an Apache server and MySQL database). So the first thing to do was to move from YAML to SQL - I chose MySQL.
If you haven't yet installed MySQL, do so before the next step. Take care to ensure that your PATH environment variable contains the MySQL binary as well as its libraries (hold down the Windows key and press PAUSE to bring up the system properties, then choose Environment Variables in the Advanced tab). For example, on my system the System PATH begins:
C:\ruby\bin;Add a new database for the application and create a user account named "ramaze" with password "TodoList" for both localhost and remote-host access:
C:\Program Files\MySQL\MySQL Server 5.0\bin;
C:\Program Files\MySQL\MySQL Server 5.0\lib\opt;
C:\Program Files\MySQL\MySQL Server 5.0\lib\debug;
...
mysql -u root -pI decided to use Sequel as my database access layer. You probably also have to install the gems mysql and sequel before the code will work - I am not sure about the mysql gem, as it was one of the things I installed before I eventually discovered the need to set up the PATH correctly.
******
CREATE DATABASE IF NOT EXISTS todolist_db;
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE,
DROP, RELOAD, PROCESS, FILE, REFERENCES, INDEX,
ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES,
LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW,
CREATE ROUTINE, ALTER ROUTINE ON *.*
TO 'ramaze'@'localhost'
IDENTIFIED BY 'TodoList' WITH GRANT OPTION;
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE,
DROP, RELOAD, PROCESS, FILE, REFERENCES, INDEX,
ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES,
LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW,
CREATE ROUTINE, ALTER ROUTINE ON *.*
TO 'ramaze'@'%'
IDENTIFIED BY 'TodoList' WITH GRANT OPTION;
quit
gem install sequelI tried choosing the 'ruby' option, but this was unable to generate the native code on my machine. So I uninstalled it and tried again, choosing the 'mswin32' option, which worked fine.
gem install mysql
Now take a safety copy of your model file, todolist.rb, and start modifying it to use MySQL instead of YAML. Replace the following lines:
require 'ramaze/store/default'with the following lines:
TodoList = Ramaze::Store::Default.new('todolist.yaml')
require 'rubygems'To begin with, I tried using the title as the primary key. But I soon found that not only was it necessary to define the title field first and then separately to name it as the primary key, the user-supplied title was not always suitable as a key value due to the presence of shell metacharacters and so on. So I decided to go with the Sequel flow and use a system-generated ID as the primary key.
require 'sequel'
DB = Sequel.mysql('todolist_db',
:user => 'ramaze',
:password => 'TodoList',
:host => 'localhost')
class TodoList < Sequel::Model(:tasks)
set_schema do
primary_key :id
varchar :title, unique => true, :null => false
boolean :done
end
end
Next I found that the model didn't support the method original(), which the original simple model supports. So I supplied one myself (subsequently found to be unnecessary after refactoring main.rb):
# Copy all records into a listMore important was to add some initialisation code after the class was defined:
def self.original
tasks = []
self.dataset.each {|r| tasks.push [r[:title], {:done => r[:done]}]}
return tasks
end
unless TodoList.table_exists?Now try running the application. It seems to work, but nothing gets stored in the database. At this point we have to bite the bullet and refactor the main module to support a more relational view of the underlying data model.
DB.transaction do
puts "Creating table 'tasks'\n"
TodoList.create_table
end
end
In the index() method, we'll start using the ID of each task to identify it instead of the title. Instead of requesting an array of key-value pairs from the original() method, we get an array of task objects from the dataset() method. That of course implies that we have to extract the title and the ID from the task object and use them as appropriate:
def indexThe methods open(), close(), task_status() and delete() have to change because they all now take an id as parameter:
@title = ["To-Do List"]
@tasks = []
TodoList.dataset.each do |task|
id = task[:id]
title = task[:title]
if task[:done]
status = 'done'
toggle = A('Open Task', :href => Rs(:open, id))
else
status = 'not done'
toggle = A('Close Task', :href => Rs(:close, id))
end
delete = A('Delete', :href => Rs(:delete, id))
@tasks << [title, status, toggle, delete]
end
@tasks.sort!
end
def delete idI decided to override the []= method of the model, so that tasks would actually be written to the database. It was an easy step from there to create a new row in the tasks table whenever the ID parameter to this method was absent or nil. So the create() method becomes:
unless TodoList.delete id
failed "Cannot delete task no.: #{id}"
end
end
def open id
task_status id, false
end
def close id
task_status id, true
end
def task_status id, status
unless task = TodoList[id]
failed "No such task no.: #{id}"
redirect_referer
end
task[:done] = status
TodoList[id] = task
end
def createNote the check for duplicates, which is easy to do now that we can look up titles in the dataset.
if title = request['title']
title.strip!
if title.empty?
failed("Please enter a title")
redirect '/new'
end
if TodoList.find(:title => title)
failed("Task '#{title}' already exists")
else
TodoList[nil] = {:title => title, :done => false}
end
end
end
Turning to the file todolist.rb, here are the methods I had to add to allow tasks to be added and deleted in the database:
def self.delete(id)That's pretty much it. But before I stopped, I wanted to prettify the user interface a bit. I didn't like the fact that the column widths tended to change whenever the sole "not done" item was added to or deleted from the list, and in any case I preferred clickable icons to text links. So I designed some icons:
puts "Attempting to delete '#{id}'\n"
DB.transaction do
if task = TodoList.find(:id => id)
task.destroy()
else
puts "Not found\n"
return false
end
end
end
# Assignment should update the underlying database
def self.[]=(id, values)
DB.transaction do
if (id == nil || !(task = TodoList.find(:id => id)))
task = TodoList.new
end
task.title = values[:title]
task.done = values[:done]
task.save
end
end
not done
done
deleteFeel free to copy the icon images. To let the Mongrel server access these, they have to be placed in the folder "public" of your project. Then I redesigned the index() method and the corresponding index.html file very slightly to use these (first declaring some constants):
DELETE_ICON = '<img src="delete_sml.gif">'The full source code is attached in the comments to this post.
NOTDONE_ICON = '<img src="notdone_sml.gif">'
DONE_ICON = '<img src="done_sml.gif">'
# the index action is called automatically when no other action is specified
def index
@title = ["To-Do List"]
@tasks = []
TodoList.dataset.each do |task|
id = task[:id]
title = task[:title]
if task[:done]
toggle = A(DONE_ICON, :href => Rs(:open, id))
else
toggle = A(NOTDONE_ICON, :href => Rs(:close, id))
end
delete = A(DELETE_ICON, :href => Rs(:delete, id))
@tasks << [title, toggle, delete]
end
@tasks.sort!
end
<p><a href="/new">New Task</a></p>
<?r if @tasks.empty? ?>
<p>No Tasks</p>
<?r else ?>
<table>
<?r @tasks.each do |title, toggle, delete| ?>
<tr>
<td class="title" > #{title} </td>
<td class="toggle"> #{toggle} </td>
<td class="delete"> #{delete} </td>
</tr>
<?r end ?>
</table>
<?r end ?>
