Creating a custom “be_valid” matcher for rspec_on_rails
February 7, 2008
I’ve been discovering the excellent ‘behaviour-driven development’ plugin ‘rspec’ , and its rails-specific cousin (nephew?) ‘rspec_on_rails’ this week.
At the moment i’m testing that validation works properly on some models. Now, rspec already generates a matcher for any predicate method that can be called on an object (eg “object.should be_foo” for “object.foo?.should eql(true)”). I was using this with valid?, which we use on an typical (ie ActiveRecord) model to see if it will pass validation (and therefore be able to be saved to the database). This is generally nicer than trying to save it and seeing if it saved ok.
So, i was writing specs like this
it "should validate with only a name" do @foo = Foo.new(:name => "bar") @foo.should be_valid end
This works, in terms of correctly passing or failing, but it doesn’t give me much information - it just tells me that something fails validation. I could get at the error messages in the test by doing the test as follows:
it "should validate with only a name" do @foo = Foo.new(:name => "bar") @foo.should be_valid @foo.errors.full_messages.should eql([]) end
but this is a bit tiresome. I’d rather just write “should be_valid”. So, i decided to write a custom matcher that overrides the auto-generated ‘be_valid’ method and tells me why it failed validation (if it does fail). Here’s how i did it. Thanks to the excellent peepcode rspec movies for the inspiration and knowhow!In my spec folder, i made a file called ‘custom_matcher.rb’. In it is a module which will hold any custom rspec matchers i write, though there’s only one at the moment.
module CustomMatchers
#checks that an AR model validates, by testing error messages from .valid?
#displays any error messages recieved in test failure output
class BeValid
#do any setup required - at the very least, set some instance variables.
#In this case, i don't take any arguments - it simply either passes or fails.
def initialize
@expected = []
end
#perform the actual match - 'target' is the thing being tested
def matches?(target)
#target.errors.full_messages is an array of error messages produced by the valid? method
#if valid? is true, it will be empty
target.valid?
@errors = target.errors.full_messages
@errors.eql?(@expected)
end
#displayed when 'should' fails
def failure_message
"validation failed with #{@errors.inspect}, expected no validation errors"
end
#displayed when 'should_not' fails
def negative_failure_message
"validation succeeded, expected one or more validation errors"
end
#displayed in the spec description if the user doesn't provide one (ie if they just write 'it do' for the spec header)
def description
"validate successfully"
end
# Returns string representation of the object being tested
def to_s(value)
"#{@errors.inspect}"
end
end
# the matcher method that the user calls in their specs
def be_valid
BeValid.new
end
end
#To hook it up, add the following require to spec_helper.rb: require 'spec/custom_matchers'
#And add the following line to the "Spec::Runner.configure do |config|" section: config.include(CustomMatchers)
This is about as simple as a matcher could be - i don’t take any arguments at all, so the expected result is always an empty array (of errors). The ‘value’ added by this is that i’m now outputting the error messages (if any) in the failure report. Because i’ve provided a description method, i don’t even need to provide a description to my specs if i don’t want to: for example, here’s a test which will fail (and its surrounding description):
describe SchoolSubscriber, ", when we make an empty school_subscriber, " do
before do
@ss = SchoolSubscriber.new
end
it do
@ss.should be_valid
end
end
And here’s the failure report from rspec:
1) 'SchoolSubscriber , when we make an empty school_subscriber, should validate successfully' FAILED validation failed with ["Phone number can't be blank", "First name can't be blank", "Position can't be blank", "School can't be blank", "Last name can't be blank", "Email can't be blank"], expected no validation errors
Sometimes a table gets messed up and the best thing to do is to wipe it and recreate it from scratch. Also sometimes, we don’t have a rake migration for it, or it was built up through a series of messy migrations that add bits, remove them, add other stuff, etc.
If we delete all the rows, we DON’T always get the same as an empty table - if, like in most rails tables, we have a primary index (usually id) which is set to autoincrement, if we delete all the records and then add one, the first row won’t be numbered 1, it will be numbered after the last row to be created. So, we’re not really starting from scratch.
A nice quick way to just recreate a blank version of the table from scratch is as follows.
1) Make an sql dump of the entire database: at the command line, type
rake db:structure:dump
2) This will create an sql script file called development_structure.sql in the db folder, which contains instructions for adding all of the tables to the db. Open it in a text viewer and look for the section concerning the table you want. Copy it out.
3) Lose the existing table - at the sql command line type “drop table your_table_name”
4) In a sql browser, or at the command line, paste in the section of sql script from the dump and run it.
Voila - nice empty table, with indexes all starting from scratch.
5) If you have any migrations to populate the table with data, you can copy the code out of the ‘up’ method, run a rails console, paste in the code and run it.
Split a class (with existing data) with single table inheritance
December 5, 2007
Let’s say that i have a table called Person, with a corresponding database table ‘people’, that already has a lot of records in it. It so happens that in my app men and women have slightly different rules associated with them, and are often searched seperately (eg search boxes have a ‘only search women’ checkbox).
The database represents gender by a field called ‘male’, which holds a boolean: men are ‘true’ and women are ‘false’. (this seems a bit sexist, but it ensures that there are only two possible values).
I’m sick of typing
:condition => ["male = ?", person.male]
all the time, and i’m generally sick of dealing with Person when there really should be two seperate classes which are dealt with differently. I want instead to be able to deal with Man and Woman, eg to sayMan.find(:all) etc, but i don’t want to start making serious changes to the database schema.
We can fix this with single table inheritance. This has two stages: 1) alter the database, and 2) extend Person into two new classes.
Stage 1 - we create a new string field in the ‘people’ table called ‘type’. This is simple with a database migration: at the command line (making sure we’re inside our project directory), type
ruby script/generate migration add_type_to_people
This should create a migration with that name.
Now, this migration has to do a bit more work than usual. We’re adding a new column, but we need to populate it as well, with either “Man” or “Woman”, which we don’t normally do in migrations. There’s nothing magical happening in migrations - we can put any rails command in there and it will get executed when the migration is run. In this case, we want to do an SQL update on all the records, and change the value in ‘type’ depending on the value of ‘male’. As it happens there’s a nice rails helper for this, called ‘update_all’. This allows us to enter the new value and an optional condition, without having to use SQL.
Looking at the ‘male’ field, we see that it happens to default to true (Come and see the sexism inherent in the system! Joking aside, if you’re going to default a boolean field, always default it to 0 (false) so that NULL and false are sort of equivalent. Anyway lets assume that whoever set this table up hadn’t had their morning coffee yet). That means that while a woman has ‘male = false’, a man might have ‘male = NULL’. With our calls to update_all, we should follow this logic, and first set all of the values of ‘type’ to ‘Man’, then set it to ‘Woman’ where ‘male = false’. So the calls to update_all will be -
Person.update_all(’type = “Man”‘)
(note the quotes here - single around the whole argument, double around the string “Man”)
Person.update_all(’type = “Woman”‘, ‘male = 0′)
Note that we compare male against ‘0′, not ‘false’. This is because the database (well mysql at least) stores bools as 0 and 1 values in a ‘tinyint’ field.
So, here’s the whole migration:
class AddTypeToPeople < ActiveRecord::Migration
def self.up
add_column :people, :type, :string
Person.update_all(’type = “Man”‘)
Person.update_all(’type = “Woman”‘, ‘male = 0′)
end
def self.down
remove_column :people, :type
end
end
Then run it with (at the command line)
rake db:migrate
That should be it for stage 1. We can check by looking in the database, either using a gui (such as heidisql) or an sql request like
select male, type from dbname.people
which will show us only the male and type columns, which we can look down to see that the values match up properly.
Stage 2 - This is easier - first we need to add two new models, Man and Woman, which extend Person. Super simple.
in app/models/man.rb
class Man < Person
end
in app/models/woman.rb
class Woman < Person
end
That’s it! We can now call methods on Man and Woman as we would on Person.
Actually, we’re not quite out of the woods yet - what happens when a new person is added to the database, or when someone whose gender was left blank is updated to be female? (ie male = false) We need to do a callback, that calls a method to correctly set the value of ‘type’ whenever a record is updated or created. For our purposes, before_save is fine.
So, in person.rb -
class Person < ActiveRecord::Base
#will be triggered when saving a new or updated object
before_save :set_type
#and now the method that the callback calls. Remember that male
#defaults (bizarrely) to ‘true’
def set_type
if self.male == false
self.type = “Woman”
else
self.type = “Man”
end
end
#or, for those who like conditional expressions -
def set_type
self.type = (self.male == false ? “Woman” : “Man”)
end
…
…
end
This should keep our database in line.
Super-simple blog app with Ajax and Restful Rails (Rails 2.0)
November 16, 2007
Thanks to rubyphunk from railsforum.com for giving me a lot of help with this. For anyone reading, i’m assuming you’ve got a basic understanding of rails already.
I’m learning ajax and restful rails at the moment, which is a hard thing to do because they both involve routing, and both have great power to confuse (me at least). So, here’s how i do a table of records which uses ajax to refresh the table when a record is deleted. I also show a way (possibly not the best way) to do a table with alternately coloured columns.
This is written in rails 1.2.5, aka ‘Rails 2.0′, but if you’re using an earlier version it should still work, provided you use the right scaffold (i’ll explain later).
Anyway, let’s get on with it - to learn about ajax and restful rails we’re going to make an app from scratch. The app is going to be super-simple - it’s a blog app which has one model, ‘Entry’ (don’t ever call a model ‘Post’ when doing restful rails. You’ll hurt your brain. Trust me) , which has a title and body field, along with the usual created and and added at timestamps. The index page will show a table of blogs, with just the start of the title and body shown, along with timestamps, and an edit and delete option - all the usual simple scaffoldy stuff.
The ajax will be used only with the delete command - when someone deletes a post, we want the table to update withoout reloading the page. To check that the page hasn’t refreshed, we’re going to put the current time on the page, which will only be updated when the page has reloaded. So, if we see the table update without the time changing, we’ve succeeded. Oh, and we’re also going to give the table vertical stripes, for no other reason than i was curious about how to do that. Let’s go.
in your rails app directory:
rails ajaxblog
cd ajaxblog
now, let’s go in and make the scaffold for our single model, ‘Entry’. This has a ‘title’ which is a string, and a ‘body’ which is text (long string). Don’t worry about the timestamp fields for now.
ruby script/generate scaffold_resource entry title:string body:text
(if you’re using rails 1.2.5, ’scaffold_resource’ won’t be recognised. Just use “scaffold” instead. scaffold_resource is the old restful scaffold, which has now become the standard in 1.2.5 onwards. I specified that here because it’s better to put in the wrong one and get a complaint, than to put in the wrong one and just get the wrong kind of scaffold made)
Now, rails has kindly generated a migration for us, so lets make a database for it to use:
mysqladmin -u root create ajaxblog_development
This is the default name specified in config/database.yml, so that’s all we need to do.
Now, go and look at the migration in db/migrate - 001_create_entries.rb:
class CreateEntries < ActiveRecord::Migration
def self.up
create_table :entries do |t|
t.string :title
t.text :body
t.timestamps
end
end
def self.down
drop_table :entries
end
end
We can see that because we told the scaffold about ‘title’ and ‘body’, it’s set up the migration for us with those fields specified. It also provides requests timestamps (updated_at and created_at) by default. So, we don’t need to do anything with the migration. I just wanted to show you something nice :)
So, close the migration without making any changes, and run
rake db:migrate
Cool. So, we should have a restful blog app now. Start a server (”mongrel_rails start” or your equivalent), go to the address specified (eg http://localhost:3000) and you should see the rails start page. Add “/entries” to the end of the url, and you should see a page with a table for entries.
So, have a play around with that. Notice that the urls are different from traditional rails - for example, the page with the list of entries would normally have been associated with an action called ‘list’ (or similar) in EntryController, and therefore have a url of /entry/list. Now it’s just /entries, which automatically calls the action “index”, which is used to provide a list of entries. This is how rest works - the urls are kept as simple and as standardised as possible, and we tell actions apart based (with some exceptions) on the type of http request rather than the method names in the controller.
Use the interface to add an entry, and you’ll be taken to the ’show’ page for the new entry. Before, the url would have been something like /entry/show/1. Now it’s simply /entries/1. So, again, we’ve missed out the action name, but the app knows what to do because when we requested the show action, we sent, via the browser, a GET request, and GET requests, when combined with a url of /entries/1, are associated with the show action. I’m not going to go into REST in too much detail here, there’s plenty of other stuff out there.
To illustrate this, let’s make our first change to the scaffold code. When someone adds their entry, we want to go straight back to the index page, to see it on the table. So, we need to edit the create action, which adds the new entry (using info from the form on the ‘new’ page) to the database.
In entries_controller.rb, in the create action, we have
# POST /entries
# POST /entries.xml
def create
@entry = Entry.new(params[:entry])
respond_to do |format|
if @entry.save
flash[:notice] = ‘Entry was successfully created.’
format.html { redirect_to(@entry) }
format.xml { render :xml => @entry, :status => :created, :location => @entry }
else
format.html { render :action => “new” }
format.xml { render :xml => @entry.errors, :status => :unprocessable_entity }
end
end
end
The respond_to block might look a bit alien - don’t worry about it for now. We’re only going to edit this line -
format.html { redirect_to(@entry) }
This is called when the record is successfully added and html is requested, which is the default file type - ie, we’re only worrying about which html page we get sent to next. Change this line to be
format.html { redirect_to entries_path }
entries_path is a helper which basically remembers the url “/entries” for us. This should take us to index after we submit a new post.
Next task - make a nicer table. In particular, lets have one that shows the created_at and updated_at data, has fixed width columns, and also has alternately coloured columns, so we can tell them apart easier.
The table is going to have two sections - the header row, where we choose names for each column, and also specify the widths, and then the record rows, which we’ll build with a for loop in the usual rails way (”for entry in @entries”, etc) . Now, the code to populate each column is going to be quite varied, because we’re not going to simply displaying the raw data. Text needs to be truncated to fit into the table, and we want our time stamps to have the format “about 1 hour ago, 2 days ago, etc”. So, we’re going to put all of this ruby code into a helper, which we call in the view. Clean views are good!
In addition, to keep things even cleaner (and to help with the ajax later) we’re going to put the table into a partial, passing through @entries as a local variable (entries, without the @) for the partial to use to populate the table.
So, edit the app/views/entries/index.rhtml (might be html.erb if you’re using the latest rails) view to look like this:
<h1>Listing entries</h1>
<%= link_to ‘New entry’, new_entry_path %>
<hr/>
<div id=”entry_table”>
<%= render :partial => “entries”, :locals => {:entries => @entries} %>
</div>
<hr/>
<%= link_to ‘New entry’, new_entry_path %>
This is all pretty standard stuff, except for the “New entry” link. Whereas previously we’d have specified an action, eg :action => “new”, we now just pass along the path helper, in this case “new_entry_path”. This is just another way to standardise things, and increase our DRYness. We don’t have to worry about the new method being renamed to “add” or something - in fact we don’t even see what the method is called, though it should be called ‘new’.
Anyway, this references a partial for the table, so lets make a new partial view for the table: “/app/views/entries/_entries.rhtml” (or html.erb) :
<table border=”0″ cellspacing=”0″>
<tr font-weight=”bold”>
<% column_names.each do |width, name| %>
<td width=”<%= width %>%” class=”<%= cycle(’table-column-header-odd’,
‘table-column-header-even’, :name => “stripes”) %>”> <%= name %> </td>
<% end %>
<% reset_cycle(”stripes”) %>
</tr>
<% for entry in entries %>
<tr>
<%# use the column_data helper to get the code for the cells %>
<% column_data(entry).each do |cell| %>
<td class=”<%= cycle(’table-column-odd’, ‘table-column-even’,
:name => “stripes”) %>”> <%= cell %> </td>
<% end %>
<% reset_cycle(”stripes”) %>
</tr>
<% end %>
</table>
This builds the table with the use of helpers, as mentioned earlier. The reason for this is two-fold:
- There’s a lot of ruby code to populate the columns with correctly formatted data. Try to avoid filling up your views with ruby code, as it scares web designers, who we want to feel relaxed (and therefore creative) when they prettify our pages. Much nicer to hide it away inside a helper.
- To achieve a striped columns effect, i’m using a rails helper called ‘cycle’, which simply cycles between a set of objects it’s given, whenever it’s called. (i don’t know how it keeps track of which one it did last…some magic). In this case, the objects it’s cycling between are some strings, which happen to be the names of styles in my stylesheet (i’ll get to those soon). Each style simply specifies a different background colour. As you can see, the call to ‘cycle’ is a bit cumbersome, and if i had it before every <td> element in a row the view would get spectacularly ugly very quickly. So, instead, i iterate through the an array of chunks of code, which is what the helper provides, and only have to write the cycle call out twice - once for the header row, and once for the record rows. Note with ‘cycle’ that you want to reset it at the end of each row, otherwise if you have an odd number of columns you’ll get a checkerboard effect. That’s why we name the cycle, so we can pass the name to the reset_cycle helper.
So, here’s the helpers used to populate the table: they live in app/helper/entries_helper.rb, which scaffold has kindly provided you with. First, the one to do the header row. This returns an array of arrays, with each subarray holding the width of the column (in percent) and its title.
#returns the width and names of the columns
def column_names
[[28, "Title"],
[28, "Body"],
[17, "Created"],
[17, "Updated"],
[5, " "], #create a column for ‘edit’
[5, " "] ] #create column for ‘delete’
end
Then, back in the partial, i call this method, get the array, and iterate through, passing the members of the subarray through as local variables to be substituted into td attributes.
column_names.each do |width, name|
The other partial works in the same way, except that there’s no subarrays - it’s just a simple array of pieces of ruby code, all bracketed up to avoid confusion. The partial takes the array, and iterates through it to get each bit of code to use to populate the relevant column. I’ll print it in full and then go through line by line, since there’s some restful stuff happening.
# Returns the pieces of code used to populate the entries index table columns
def column_data(obj)
[(link_to truncate((h obj.title), 35), obj),
(truncate((h obj.body), 35)),
("#{time_ago_in_words(obj.created_at)} ago"),
("#{time_ago_in_words(obj.updated_at)} ago"),
(link_to 'Edit', edit_entry_path(obj)),
(link_to 'Delete', entry_path(obj), :method => :delete ) ]
end
Now lets break this down line by line.
def column_data(obj)
‘obj’ is the object passed through from the partial - in this case, an ‘Entry’ object, from the “for entry in entries” line in the partial. So, whenever we see ‘obj’ it’s as if we’re saying ‘entry’. I know that seems obvious but some of the rest calls might be a bit unfamiliar and it’s easier to understand if you mentally substitute ‘entry’ in there.
[(link_to truncate((h obj.title), 35), obj),
link_to tells us this is a link - in this case, if someone clicks on the title i want them to be taken to the 'show' page for that entry. All that's required for 'show' is to pass through the object to be shown - rails assumes that it's an entry since we don't ask for a specific path.
truncate simply truncates the text to the specified number of chars and puts an ellipsis "..." at the end.
We say (h obj.title) instead of simply (obj.title) as a security measure. h is a helper which treats text as raw text, rather than trying to use it as html, which the browser might otherwise try to do. This is a simple security measure - it means that if (for example) someone enters a ton of javascript as their blog post in order to hack our site, then all that happens is that they have a blog post full of javascript code - it doesn't run it. At least, that's my understanding of it :)
(truncate((h obj.body), 35)),
("#{time_ago_in_words(obj.created_at)} ago"),
("#{time_ago_in_words(obj.updated_at)} ago"),
These are all pretty simple - time_ago_in_words is just another helper for formatting a time value into text such as "about 1 hour". You need to add " ago" yourself at the end for the desired effect in this case.
(link_to 'Edit', edit_entry_path(obj)),
Again, rather than passsng :action => "edit", :id => obj, as we would in traditional rails, here was just call the relevant path helper with the desired object.
(link_to 'Delete', entry_path(obj), :method => :delete ) ]
This is more RESTful stuff - notice the “:method => :delete” at the end? This tells the browser to send a DELETE request (well, actually a pseudo-delete request since browsers aren’t allowed to send delete requests. Rails understands :) For the purposes of *our* understanding of REST, think of it as an http DELETE request. When the controller gets a delete request, and is given the path to an object (which will equate to /entries/obj_id), then it calls the ‘destroy’ action on that object, in the relevant controller - entry_controller in this case, since we give it ‘entry_path’. The scaffold provided us with a perfectly working destroy action in our controller, so we don’t need to worry about it any more (for now).
end
Finally, before i forget, let’s get the styles in there. Make a new stylesheet in /public/stylesheets called ‘ajaxblog.css’, and put the following styles into it -
.table-column-even {
background: #f0f0f0;
}
.table-column-odd {
background: #f8f8f8;
}
.table-column-header-even {
font-weight: bold;
background: #f0f0f0;
}
.table-column-header-odd {
font-weight: bold;
background: #f8f8f8;
}
(i know next to nothing about stylesheets, and i think this isn’t the official way to set them up. But hey ho it works.)
And of course, we need to make sure that our layout knows about the new stylesheet. So, go and open app/views/layouts/entries.rhtml, and add this line in the head element, next to the scaffold stylesheet reference -
<%= stylesheet_link_tag “ajaxblog”, :media => “all” %>
While you’re there, add this line as well, below the css references in the head. We’ll need this for our ajax later but since we’re here now we might as well -
<%= javascript_include_tag :defaults %>
OK! That was quite a lot of stuff to do in a single iteration, so go and check that it all looks ok. Table nice and stripy? (you can change the colours in the stylesheet). Delete and edit work? Adding a new entry takes you back to the index? Good.
So, now that we’ve got our restful app working, let’s do some ajax. We’re going to be changing the functionality of ‘delete’, so that it just updates the table, rather than reloads the page. But how can we tell the difference? It’s hard in this case, with such a simple app, because the page will tend to reload so quickly that we might not even see it happen. So, let’s add the time to the page - that way, if the page is reloaded, the time will change. (this might not be the best demo of ajax but it sure is simple)
In app/views/entries/index.rhtml, add the following, below the <h1> line -
<span>Current time: <%= Time.now.strftime(”%H:%M:%S”) %> </span>
<br/><br/>
This just shows the local system time in 12:34:56 format. Try refreshing the browser a few times to check it works. So, we want to update the table without changing this value. Let’s (FINALLY) do some ajax.
First thing to do is to change what happens when the user clicks on the ‘delete’ link. We need to change it to make a remote ajax call. This involves replacing the link_to helper with another helper, called “link_to_remote”. Here’s the old line which, remember, is hidden away in entries_helper.rb:
link_to ‘Delete’, entry_path(obj), :method => :delete
And here’s the new one -
link_to_remote ‘delete’, :url => entry_path(obj), :method => :delete
As you can see, it’s not radically different. Instead of passing entry_path, we point :url at entry_path. But the link_to_remote means that we’ve got an ajax call happening.
Next, we need to go and look at the destroy method in entries_controller, which is called by a DELETE request on an entry object, remember. This is what it looks like at the moment:
# DELETE /entries/1
# DELETE /entries/1.xml
def destroy
@entry = Entry.find(params[:id])
@entry.destroy
respond_to do |format|
format.html { redirect_to(entries_url) }
format.xml { head :ok }
end
end
If you’re new to REST, this will look quite unfamilar once we get to the “respond_to” line. I’ll get to that in a minute.
First, we need to change the controller to provide an updated value for @entries, which we’re going to pass through to the updated table in a minute. So, after the line that destroys @entry, we update @entries:
@entries = Entry.find(:all)
Now, the respond_to block - what’s happening here is that we specify what data we can supply in response to various formats of request. I’m not getting into the xml here - let’s just worry about the normal case, when html is requested. What the controller would do in that case is call the format.html line, which has a block of code that simply redirects back to the index page, ie reloads the page.
If we got an ajax request, we don’t want to redirect - we’re planning on reloading just the table. So we need to stop it from redirecting if we got an ajax request. The way to do this is simple - change the line as follows:
format.html { redirect_to(entries_url) unless request.xhr? }
Our final change is to make sure that when we fall out the bottom of the controller (as we will do now that we no longer redirect anywhere), rails looks for some javascript to run. (at least, that’s my understanding, i could be wrong). So, at the bottom of the list of formats, just add
format.js
So, our new destroy action looks like this:
# DELETE /entries/1
# DELETE /entries/1.xml
def destroy
@entry = Entry.find(params[:id])
@entry.destroy
@entries = Entry.find(:all)
respond_to do |format|
format.html { redirect_to(entries_url) unless request.xhr? }
format.xml { head :ok }
format.js
end
end
The “request.xhr?” part will return true if the action was called by an ajax request. In this case, if the request isn’t an ajax request, we deal with updating the table the old fashioned way - by simply reloading the whole page with a redirect_to call. Putting this test, using request.xhr?, into our ajaxy actions is a good habit to get into, as it allows you to deal with the case where the user doesn’t have javascript enabled in their browser, and to do it another way. This is a good reason, when planning on writing some ajax, to get the functionality working first using the old-fashioned page reloading. That way, it’s easy to fall back onto the non-ajax functionality if the user can’t, or doesn’t want to, use javascript.
However, if it is an ajax call, then nothing happens, and we just drop out of the bottom of the method. Now what?
This is where the really clever stuff comes in. Rails has a load of javascript helpers that help us to use ajax without actually using the required javascript. They go into rjs (ruby javascript) files, that live in the view folder corresponding to the controller holding the action, and are named the same as the action. An, because we added format.js to the bottom of the respond_to block in the controller, when we fall out the bottom of the controller method rails will look for an rjs file called “destroy.rjs”.
So, in /app/views/entries, create a new file called “destroy.rjs”, and add to it the following line:
page.replace_html ‘entry_table’, :partial => “entries”, :locals => {:entries => @entries}
Let’s see what’s going on here -
- “page” is an object provided to us by rails to work on. It corresponds to a page of html, from which we pull out individual components, identified by their “id” attribute.
- replace_html is one of those nice javascript helpers i mentioned. It replaces the element with the id that follows (”entry_table”, which is what we labelled the div that surrounds the call to the table partial, in index.rhtml)
- So, what do we replace it with? We replace it with a partial, by specifying :partial. Generally i believe it’s the common practise to use ajax to replace a whole partial rather than bits and bobs here and there. It might even be essential (i’m new to this myself and not sure). The partial is of course “entries”, which contains the table. So, we’re saying ‘replace the table with an updated version of the table’.
- Finally, we pass through to the partial the same parameters that we passed through when we call it from the index page: “:locals => {:entries => @entries}”. In this case, @entries has been updated by the ‘destroy’ method in the controller, and now no longer contains the record we just deleted.
The end result of all this is that the partial with the table gets reloaded, with a new value of @entries to dutifully iterate through, and we see our deleted record disappear, without the page being reloaded (make sure the time doesn’t change!).
So there you go. Add ajax and RESTful rails to your skills list and go and make a celebratory cup of tea. Of course, i’ve barely grazed the iceberg of ajax or REST here, so go and read some more about both of them while you drink it.
Submission day tomorrow!
September 23, 2007
I’ve been too busy to write any new blogs. Even the one i posted earlier today had been sitting on my laptop for nearly a week and a half till i got round to hitting the ’submit’ button. Tomorrow is deadline day, so i’m finishing off a few bits and pieces.
Going on holiday for a few days afterwards, can’t wait. :)
I can see the head!
September 23, 2007
If a rails app were a baby, the pregnancy would be wonderful and the labour would be a horrible, painful affair with the blood, and the screaming, and the fainting nurses, and the obstetricians working shifts. I’m at the stage now where the head is mostly out, but those wide shoulders are causing everyone a lot of trouble.
What i’m trying to say is - it’s up! At www.reeplies.com. I’ll fix the route for this later so reeplies.com works, but for now it’s best hidden away as it’s pretty broken - basically, gems don’t work. I can’t *install* my own gems on the server - this would be the ideal way. What i need to do is to ‘freeze’ them into local files (that live in the vendors folder) and then copy those files over.
For now, you can add articles, as long as you put the title in (i commented out the bit where reeplies says you can leave title blank). However, if you try and *view* an article, the code breaks because it needs the rmagick gem to show pictures.
Yesterday, James McCarthy gave me way too much of his time trying to get the deployment working. We were having all sorts of problems, with not very much (if any) feedback about what the problems were. The app was just broken. No trace or nothing. In the end, i built up a new app from scratch, following bluehost’s instructions, then made the structure the same as mine (with ./script/generate controller article etc for every controller and model), then replaced all the empty files with my app’s files, doing the same thing for the public folder. And that works, except for like i say, the gems are missing.
Still though, feel massively buoyed by the fact that the damn thing is working in a limited way. I’m confident i can solve the gems problem as well, with james’ help and some forum action - lots of people must have had this exact problem before.
I already tried getting a tar file (destined for *nix surely), copying it over the the app vendor folder via ftp, then untarring it on the server, via ssh - this seemed to work, in that it untarred to the correct place, but the app still crashes.
Next step -after following a tutorial on the bluehost site, i changed my .bashrc file, which lives in the root directory of my allocated folder, by adding the following:
export PATH=”$PATH:$HOME/packages/bin:$HOME/.gems/bin”
export GEM_HOME=”$HOME/.gems”
export GEM_PATH=”$GEM_HOME:/usr/lib/ruby/gems/1.8″
export GEM_CACHE=”$GEM_HOME/cache”
Then, following some forum advice, i typed
source ~/.bashrc
at the command line - this makes the bashrc file get read i think.
After that, i can install gems! Or at least, i could install hpricot, i didn’t try anything else.
However, the app still crashes when i require hpricot.
Looking at another forum post, it may be that the server doesn’t know where to look for them. They are in ~/.gems/gems (where ~ is the root folder)
James with some advice again, pointed me here.
Following this, i do the following. Go to my root and type ‘pwd’ - this gives me
/home/reeplies
- the location of my root folder. Next, i type
gem environment
and get the following
RubyGems Environment:
- VERSION: 0.9.2 (0.9.2)
- INSTALLATION DIRECTORY: /home/reeplies/.gems
- GEM PATH:
- /home/reeplies/.gems/gems
- /home/reeplies/.gems
- REMOTE SOURCES:
- http://gems.rubyforge.org
Then i make a new file called gemrc in the root directory, and put this into it:
gemhome: /home/reeplies/.gems/gems
gempath:
- /home/reeplies/.gems
- /home/reeplies/.gems/gems
Next, call
gem environment
again:
RubyGems Environment:
- VERSION: 0.9.2 (0.9.2)
- INSTALLATION DIRECTORY: /home/reeplies/.gems
- GEM PATH:
- /home/reeplies/.gems/gems
- /home/reeplies/.gems
- REMOTE SOURCES:
- http://gems.rubyforge.org
wait - i didn’t change anything! The environment’s the same! Never mind, carry on.
Next, i added the gem paths into environment.rb with this line:
ENV['GEM_PATH'] = ‘/home/reeplies/.gems:/home/reeplies/.gems/gems’
I’m guessing that the colon seperates items in a list of paths.
Next step: edit .bash_profile, which is again in the root directory. Added the following lines:
export GEM_PATH=/home/reeplies./gems/gems
export GEM_HOME=/home/reeplies/.gems
WAIT - i’ve been an idiot. I need to include the existing preinstalled gems as well - these are the paths for those:
gems folder is in “/usr/lib/ruby/gems/1.8/“
gem itself (the program) is in “/usr/sbin“
and my local path is ‘/home/reeplies/.gems‘ (i think we miss off the last ‘gems’ as that’s the thing we’re locating).
Lets try again.
…
nope. OK, let’s try again with the extra /gems added to the paths - so they point to where the individual gems folders are rather than where the ‘gems’ folder is. So, this time
gems folder is in “/usr/lib/ruby/gems/1.8/gems/“
local path is ‘/home/reeplies/.gems/gems‘
OK, here we go again…
nope.
tried again with
gems folder is in “/usr/lib/ruby/gems/1.8“
local path is ‘/home/reeplies/.gems/gems‘
still no luck. arghhh.
james has fixed it, bless him. What a star. It was something to do with not using the nested gems folders after all i think.
…
made some more progress:
“require ‘model_extensions’ ” wasn’t working because i had to copy over model_extensions.rb from my lib folder. Once done, this worked. Next problem was
“require ‘open-uri’ “. Open-uri is a standard component of ‘rubygems’, a list of standard gems usually installed along with rails/ruby, but it’s not in the installed version for some reason. Fortunately, i was able to get it by getting a list of all available gems with
gem list –remote >remote_gems_list.txt
and then doing
gem install ‘rubygems-update’
Sorted!
So, plugin/gem-wise, all i’ve got left is
#include Magick
#gem ‘actionpack-imagemagick’
#require ‘mini_magick’
Next step - did
]# script/plugin install http://opensvn.csie.org/rails_imagemagick_tag/imagemagick_tag/
Which installs imagemagick for rails. Testing - yes, my pictures work! Woohoo! (next stop movies, don’t want to think about that for now though). One problem with the pictures is that the thumbnail’s aren’t showing up. Let’s have a look at that…my local version’s showing them fine, maybe i didn’t set the thumbnail folder up? Nope, the folder is there, and there’s a correctly saved and named thumbnail in there. Let’s have a look at the page source (always a good start for debugging:
<img alt="Missing thumbnail" src="/data/pictures/thumbs/thumbpic1.jpeg?1190030080" />
Ah - see all those digits stuck on the end of the filename? I had that problem before! Argh, what was the solution…i remember getting help on a forum about it. Maybe i blogged it? Right - it’s something to do with the image_cache. It’s not the digits themselves, as my working local version has those as well, when i look in the source.
Found it - i’d commented out the following lines in the deployed version as they were making it crash -
imagemagick_for ‘/data/pictures’, :cache => ‘/data/pictures/cache’
On the web - sort of
September 14, 2007
My app’s up but not quite running. I’m getting this when i go to the beta url (which is http://beta.reeplies.com/public) :
Application error
Rails application failed to start properly”
Yesterday, Jay Gooby from the Brighton Ruby group kindly gave me several hours of his time to try and get Reeplies up and running. Here’s a breakdown of what i’ve done so far, before meeting Jay and with his help, as much for my own benefit as anyone elses.
Got my account with bluehost, which included free domain name registration with a years’ hosting (which cost about £45).
Got them to give me SSH access (i needed to provide some id, i emailed photos of the front and back of my driving license which was fine). They provided me with an ssh login and password. I learned that SSH is basically like telnet - a way of communicating with a remote host via the command line The difference is that the communication is encrypted, but that all happens under the hood - from a users perspective it’s like telnet.
Using the bluehost control panel, added an ftp account “max_williams@reeplies.com”. This created a subfolder called /public_html/max_williams.
Jay added another ftp account “beta@reeplies.com” which has the folder /rails/beta. This folder is to hold my working rails application folder.
Next step was to copy the application folder over! Bluehost provide their own ftp client for this but it’s a java applet and is a bit clunky. We used CoreFTP which i had already installed. We logged into beta@reeplies.com, and then just dragged the ‘newspipe’ (the working name of Reeplies) folder into the root directory (ie into rails/beta).
We were doing this using the wifi in the Windmill on montpelier terrace (nice pub btw - well kept ale and great chips) so the upload took a long time. Tip for anyone doing this - copy your app folder over to a temporary location and clear out anything you don’t need - saved files from the public folder and svn files for example. Then upload the cleaned up copy instead of your original folder. Obviously the PROPER way to do it is to get an online svn repository and install it from there. I’m working on it, for now the priority is to get SOMETHING up there.
Anyway, next step is to deal with Ruby gems and plugins: the host doesn’t have the ones i use, so i need to install them myself. According to Jay this is simple - we just go into the host unix environment (using ssh) and treat it like we would our own local command line. One difference is that the host is a unix environment whereas i’m used to windows: that means that some or all of the plugins/gems will use different versions to the ones i used.
To connect to the ssh command line i used PuTTY, with these connection settings:
Host name = reeplies.com
port = 22
connection type = SSH
(putty lets you save these details for future use)
So, now we’re on the remote server’s command line! Time for me to do a little bit of research into unix commands - i’ve never really ‘done’ unix. OK…
cd dirname - open directory - same as windows.
cd .. - go up - again same as windows
ls - list files and folders - like dir in windows
ok…
Next step was to get a mysql database up for my app to use. Created this using the bluehost control panel - a new mysql database called ‘reeplies_newspipedevelopment’ (it wouldn’t let us put underscores in for some weird reason, even though it adds the “reeplies_” prefix itself. Then, after the database.yml file had copied over, Jay changed it to point to the new database, and also set it so that it won’t be overwritten when i copy my files over.
I’m *not sure* whether we actually managed to set the database up, ie create all the tables and fields using my schema. There was an issue with rake - it didn’t seem to want to work. I think that this was maybe just because the files were still copying over the ftp. I just tried it now, but it’s having a problem because hpricot isn’t installed. hpricot is one of my gems (or is it a plugin?), so i guess all the gems and plugins need to be there before the migrate can happen.
Hmm. Think i’ll try commenting out those lines from environment.rb for now to see if i can get the migration to work…nope, now it has a permissions issue:
Access denied for user ‘max’@'localhost’ (using password: YES)
I’m a bit nervous about flailing around with this in case i break something. Think i’ll leave it till i get my next expert help session tomorrow from James McCarthy.
So, might as well get on with installing the gems and plugins! According to my environment.rb file, this is what i’m using (in future it would be a good idea to put in a comment next to them with the precise command i used to install them):
require ‘rubygems’
require ‘RMagick’
require ‘open-uri’
require ‘hpricot’ - used to get the title of a web page from a url
include Magick
gem ‘actionpack-imagemagick’
require “mini_magick”
Now, i remember reading about a ruby gem that basically takes all of your gems, and maybe plugins, that you’re using and does something with them that makes them easy to copy over. Looks like this is it - Rick Olsen’s Tzinfo: http://agilewebdevelopment.com/plugins/gems
Looks a bit complex - or rather, i’m nervous :) Going to try installing them one by one. Let’s do hpricot first as i think that was pretty simple (unlike rmagick/imagemagick).
hpricot: seems simple enough - just type
gem install hpricot
Gah, foiled again: i get a choice of versions (windows, jruby, ruby) and when i choose ruby i get:
ERROR: While executing gem … (Errno::EACCES)
Permission denied - /usr/lib/ruby/gems/1.8/cache/hpricot-0.6.gem
OK - time to mail the bluehost people i think…
OK - before i post my question, they suggest some previous replies that may be helpful. One of them is “How do I install my own Gems?”. Let’s have a look.
OK - according to this, i do the following: use their file manager to open .bashrc, which is in the root directory (back it up first). Then add the following lines to the end:
export PATH=”$PATH:$HOME/packages/bin:$HOME/.gems/bin”
export GEM_HOME=”$HOME/.gems”
export GEM_PATH=”$GEM_HOME:/usr/lib/ruby/gems/1.8″
export GEM_CACHE=”$GEM_HOME/cache”
Then connect to the site with ssh (i’m in there already), and enter the following in the root folder:
cp /usr/lib/ruby/gems/1.8/cache/sources-0.0.1.gem ./
gem install sources-0.0.1.gem
gem update -y
Did the first line, got a permission denied error again. OK, i’m mailing them now! Arggh, they’ve got no option to edit your ‘ticket’ ie your help request - it’s either send it or cancel it and start again! That’s really annoying. Going to have to rewrite it as i want to tell them that this other suggestion doesn’t work.
My app’s up but not quite running. I’m getting this when i go to the beta url (which is http://beta.reeplies.com/public) :
Application error
Rails application failed to start properly”
Yesterday, Jay Gooby from the Brighton Ruby group kindly gave me several hours of his time to try and get Reeplies up and running. Here’s a breakdown of what i’ve done so far, before meeting Jay and with his help, as much for my own benefit as anyone elses.
Got my account with bluehost, which included free domain name registration with a years’ hosting (which cost about £45).
Got them to give me SSH access (i needed to provide some id, i emailed photos of the front and back of my driving license which was fine). They provided me with an ssh login and password. I learned that SSH is basically like telnet - a way of communicating with a remote host via the command line The difference is that the communication is encrypted, but that all happens under the hood - from a users perspective it’s like telnet.
Using the bluehost control panel, added an ftp account “max_williams@reeplies.com”. This created a subfolder called /public_html/max_williams.
Jay added another ftp account “beta@reeplies.com” which has the folder /rails/beta. This folder is to hold my working rails application folder.
Next step was to copy the application folder over! Bluehost provide their own ftp client for this but it’s a java applet and is a bit clunky. We used CoreFTP which i had already installed. We logged into beta@reeplies.com, and then just dragged the ‘newspipe’ (the working name of Reeplies) folder into the root directory (ie into rails/beta).
We were doing this using the wifi in the Windmill on montpelier terrace (nice pub btw - well kept ale and great chips) so the upload took a long time. Tip for anyone doing this - copy your app folder over to a temporary location and clear out anything you don’t need - saved files from the public folder and svn files for example. Then upload the cleaned up copy instead of your original folder. Obviously the PROPER way to do it is to get an online svn repository and install it from there. I’m working on it, for now the priority is to get SOMETHING up there.
Anyway, next step is to deal with Ruby gems and plugins: the host doesn’t have the ones i use, so i need to install them myself. According to Jay this is simple - we just go into the host unix environment (using ssh) and treat it like we would our own local command line. One difference is that the host is a unix environment whereas i’m used to windows: that means that some or all of the plugins/gems will use different versions to the ones i used.
To connect to the ssh command line i used PuTTY, with these connection settings:
Host name = reeplies.com
port = 22
connection type = SSH
(putty lets you save these details for future use)
So, now we’re on the remote server’s command line! Time for me to do a little bit of research into unix commands - i’ve never really ‘done’ unix. OK…
cd dirname - open directory - same as windows.
cd .. - go up - again same as windows
ls - list files and folders - like dir in windows
ok…
Next step was to get a mysql database up for my app to use. Created this using the bluehost control panel - a new mysql database called ‘reeplies_newspipedevelopment’ (it wouldn’t let us put underscores in for some weird reason, even though it adds the “reeplies_” prefix itself. Then, after the database.yml file had copied over, Jay changed it to point to the new database, and also set it so that it won’t be overwritten when i copy my files over.
I’m *not sure* whether we actually managed to set the database up, ie create all the tables and fields using my schema. There was an issue with rake - it didn’t seem to want to work. I think that this was maybe just because the files were still copying over the ftp. I just tried it now, but it’s having a problem because hpricot isn’t installed. hpricot is one of my gems (or is it a plugin?), so i guess all the gems and plugins need to be there before the migrate can happen.
Hmm. Think i’ll try commenting out those lines from environment.rb for now to see if i can get the migration to work…nope, now it has a permissions issue:
Access denied for user ‘max’@'localhost’ (using password: YES)
I’m a bit nervous about flailing around with this in case i break something. Think i’ll leave it till i get my next expert help session tomorrow from James McCarthy.
So, might as well get on with installing the gems and plugins! According to my environment.rb file, this is what i’m using (in future it would be a good idea to put in a comment next to them with the precise command i used to install them):
require ‘rubygems’
require ‘RMagick’
require ‘open-uri’
require ‘hpricot’ - used to get the title of a web page from a url
include Magick
gem ‘actionpack-imagemagick’
require “mini_magick”
Now, i remember reading about a ruby gem that basically takes all of your gems, and maybe plugins, that you’re using and does something with them that makes them easy to copy over. Looks like this is it - Rick Olsen’s Tzinfo: http://agilewebdevelopment.com/plugins/gems
Looks a bit complex - or rather, i’m nervous :) Going to try installing them one by one. Let’s do hpricot first as i think that was pretty simple (unlike rmagick/imagemagick).
hpricot: seems simple enough - just type
gem install hpricot
Gah, foiled again: i get a choice of versions (windows, jruby, ruby) and when i choose ruby i get:
ERROR: While executing gem … (Errno::EACCES)
Permission denied - /usr/lib/ruby/gems/1.8/cache/hpricot-0.6.gem
OK - time to mail the bluehost people i think…
OK - before i post my question, they suggest some previous replies that may be helpful. One of them is “How do I install my own Gems?”. Let’s have a look.
OK - according to this, i do the following: use their file manager to open .bashrc, which is in the root directory (back it up first). Then add the following lines to the end:
export PATH=”$PATH:$HOME/packages/bin:$HOME/.gems/bin”
export GEM_HOME=”$HOME/.gems”
export GEM_PATH=”$GEM_HOME:/usr/lib/ruby/gems/1.8″
export GEM_CACHE=”$GEM_HOME/cache”
Then connect to the site with ssh (i’m in there already), and enter the following in the root folder:
cp /usr/lib/ruby/gems/1.8/cache/sources-0.0.1.gem ./
gem install sources-0.0.1.gem
gem update -y
Did the first line, got a permission denied error again. OK, i’m mailing them now! Arggh, they’ve got no option to edit your ‘ticket’ ie your help request - it’s either send it or cancel it and start again! That’s really annoying. Going to have to rewrite it as i want to tell them that this other suggestion doesn’t work.
Next day…still waiting for a reply. bah.
A little bit more polishing and then back to the hard stuff
September 5, 2007
One last thing i want to do - i want to add a new page where you can see all the reeplies, listed by score and date (like the article list). This uses a paginator, like the article list.
By basically copying the article code (i’m not going to reproduce it all here), this is all working. However, it looks bad - the format used to list stories at the bottom of the article doesn’t look so good once they’re a big list using the whole screen. In addition, each story/reeply should have a link to the article to which it replies.
So, i need a new format partial, to list stories and show the associated article as well. This one will be called ’story_and_article’, and the format will be closer _article than it is to the existing _story partial. Here it is:
<% story = story_and_article %>
<tr>
<td>
<%= render :partial => ’story/updown_arrows’,
:locals => { :story => story } %>
</td>
<td class=”poster_name”>
<%= story.points_to_s %>
</td>
<td>
<span class=”article_list”><%= link_to story.title,
:controller => ’story’,
:action => “show”,
:id => story %></span>
<span class=”summary_list”> Reeplying to <%= link_to story.article.title,
:controller => ‘article’,
:action => “show”,
:id => story.article %></span>
<br/>
<% unless story.summary == nil || story.summary == “” %>
<%= render :partial => ’shared/summary_brief’,
:locals => { :item => story } %>
<br/>
<% end %>
<%= render :partial => ’shared/added_by’,
:locals => { :item => story } %>
<br/>
</td>
</tr>
Note that the first thing that i do is take the input object “story_and_article” and rename it to “story”, just so i’m not typing stuff like “story_and_article.article.summary”. The rest of it is pretty standard - note that i’m calling another two partials for the summary and ‘added_by’.
The summary partial is quite interesting, as it uses a helper method to show only the first n words of the summary: here’s the partial -
<span class=”summary_show”>”<%= first_x_words(item.summary, 20)%>”</span>
Minimal eh? I only split this into a partial in case i want to add a summary to articles as well - that’s why it works on an ‘item’, not a ’story’.
Here’s the method it calls:
def first_x_words(str,n=20,finish=’…’)
truncated = str.split(’ ‘)[0,n].inject{|sum,word| sum + ‘ ‘ + word}
truncated += finish if truncated.length < str.length
return truncated
end
This uses the ruby inject method, which i *have* understood in the past but haven’t used before. It’s a bit like a recursive method call i think. In case you’re wondering, … is n html symbol for “…” - it’s neater than actually writing “…”: a bit smaller and renders better across all platforms apparently.
The ‘added_at’ partial is the same code i’ve been reusing in various places - i just partialled it out for DRYness: again it uses ‘item’ since it IS used by article and story. Note that in the story_and_article code above i pass through a local variable, setting :item to be equal to ’story’.
<span class=”poster_name”>
<%= “Added” if item.added_at or item.user_id %>
<%= “#{time_ago_in_words(item.added_at)} ago ” if item.added_at %>
<%= “by #{item.user.login}” if item.user%></span>
Done a bit more tweaking and polishing…
Found a bug with those submit_tag confirmation dialogs from yesterday - the box that’s displayed says “Submit Reeply “my reeply title”?”, getting the title from the saved object. But, the title hasn’t been saved to the object yet, so it displays the old name (eg the default of “untitled reeply”). I need to get the data out of the form field rather than the object.
OK, with some more forum help (the forums have been real good to me recently, probably because i’m doing run of the mill stuff rather than arsey video conversion), i just need to change the :onclick parameter:
<%= submit_tag “Delete Reeply”,
:onclick => “return confirm(’Delete \”‘ + getElementById(’story_title’).value + ‘\”?’);” %>
The key here is “getElementById” - the camelized name should be a giveaway that this isn’t ruby, it’s javascript. Good to know.
Just noticed something else - on the article list page, if an article has 1 Reeply, it says “1 Reeplies”. Need to adjust my number_of helper method to take pluralization (or lack of it) into account:
def number_of(params = {})
word = params[:word]
number = params[:number].to_i
if number != 1
word = word.pluralize
end
if number == 0
if params[:capitalize]
number = “No”
else
number = “no”
end
end
return number.to_s + ” ” + word
end
so, a an article with stories = 0 has “No Reeplies”, one with stories = 1 has “1 Reeply”, one with stories = 2 has “2 Reeplies”. I’ve left points as being displayed as “0 points” rather than “No points” as points can go to minus numbers, so 0 is a number, unlike Reeplies where 0 means an absence of something.
I think i’m just about done with the tweaking. Now, to get back to the video stuff again. Whoops, it’s 6 o clock already, time flies when you’ve not got much of it left.
More tweaking
September 5, 2007
At the moment, i’m avoiding the nasty video conversion and deployment problems, and just focussing on improving Reeplies. Feels like shirking but hey (i’m also avoiding writing up my project). The next thing i’m going to do is to add a page for “Your draft Reeplies”, ie “stories where user_id = session[:user].id and complete = 0″. Ideally i’d do a tabbed page but this is unneccessary polish at this stage.
The drafts page is simply going to be a list of stories. To stay DRY i want to use the story_list partial, but there’s a problem - currently the story list partial defines what makes it into the list of stories, which won’t work for drafts as the criteria are different. I think it would be better if story_list works with a variable called ’story’, and i define ‘for story in @some_stories’ before calling the partial.
Looking in my copy of ‘Agile web dev with RoR’, i see the perfect answer: if you call a partial and pass a :collection value through, then the contents of the partial get called for every item in the collection. In order for this to work, the name of the individual items in the partial should be the same as the name of the partial. To avoid rewriting the partial, where i’m constantly referring to ’story’, i’m going to rename the partial to be ’story’.
So, the new _story partial is the same, but minus the for and end lines. I now call i like this from article/show:
<%= render :partial => ’story/story’, :collection => @article.complete_stories %>
and like this from story/list_drafts:
<%= render :partial => ’story/story’, :collection = @drafts %>
I’ve done the same for the other partials that list stuff - i’ve moved the conditions for what gets listed outside of the partial, as above.
OK. Next step is to show the drafts. First, i set up a new relation for User, ‘drafts’, which is all stories belonging to them where complete = false. Then, if i have @user, i can just say @user.drafts. In user.rb:
has_many :drafts,
:class_name => “Story”,
:conditions => [ 'complete = ?', false ],
:order => “added_at DESC”
Next, i add a link to the “logged in” partial, so you see
You are logged in as patton Log out Change password Your Draft Reeplies
at the top of the page. This is the link:
<%= link_to ‘Your Draft Reeplies’, :controller => “story”, :action => “list_drafts” %>
Next i need a story controller method to set up a couple of variables:
def list_drafts
@user = User.find(session[:user].id)
@drafts = @user.drafts
end
Then finally the view page for the list of drafts:
<%= render :partial => ’shared/save_page’ %>
<%= render :partial => ‘user/login_manager’ %>
<span class=”page_header”>Your draft Reeplies</span>
<br/>
<% @stories = @drafts%>
<%= render :partial => ’story/story_list’ %>
Looks good, but there’s a problem: when you delete a draft, it takes you to the article page that the draft reeply was associated with. I want it to stay on the draft reeplies page.
I’m already saving the page with the save_page partial, which simply has this:
<% session[:return_to] = request.request_uri %></div>
So, it should simply be a case of making sure that the delete action goes to the saved page after doing it’s stuff. The story/delete action looks like this:
def delete
@story = Story.find(params[:id])
@article = @story.article
@story.destroy
redirect_to :controller => “article”, :action => “show”, :id => @article
end
session[:return_to] is used by the application controller method redirect_to_stored. So, if i just change the last line to
redirect_to_stored
then it should work fine. It does! Nice.
Did a little bit of tidying up with partials - the login_manager is now part of the standard layout, and has a Reeplies.com link, which goes to the article list page. At some point it would be nice to replace this with a little logo.
Another thing i’ve been meaning to do for ages is to put a confirmation dialog on delete actions. This is simple enough: along with the parameters for a link_to, we pass a k-v pair with the key :confirm, and the value for whatever we want to be displayed for the confirmation question, eg “Delete this Reeply?”. One gotcha with this is that it goes in a different hash to the one that has :controller, :action, and :id, so we need to put those in { }.
(personally i think rails would be clearer if we always used the { }, but the convention is to omit them whenever possible. To me that highlights the problem with cases like this, where it breaks if you omit them).
Anyway, the link to delete now looks like this:
<span class=”option_link”><%= link_to “Delete \”#{story.title}\”",
{ :controller => “story”, :action => “delete”, :id => story.id },
:confirm => “Delete Reeeply: are you sure?” %></span>
I added this option into the story show page, and of course it crashed because it was trying to go back to the show page for a story that doesn’t exist. So, when i call delete from story, i need to go to the article show page instead.
The way i’ve done this (which may not be the most elegant way) is to do a regexp check on session[:return_to] in the controller, and reroute to the article/show if it was going to go back to story/show:
if session[:return_to].match(/reeply\/show/)
redirect_to :controller => “article”, :action => “show”, :id => @article
else
redirect_to_stored
end
Ah, wait - what if they came to story/show from the drafts page? Deleting the story should take you back to the drafts page. Hmm.
I wonder if i could handle this a bit more elegantly? Maybe on every page i could push the current url onto a stack, and if there’s a problem with the current page, go back down the stack until i find one that works? Someone must have thought of this stuff for rails already though…i think, instead of doing a massive stack, i’ll just have the previous and the page before that, using the same save_page system i’ve got at the moment. This is the new save_page partial:
<% session[:return_to_older] = session[:return_to]%>
<% session[:return_to] = request.request_uri %>
This is the new redirect_to_stored method:
def redirect_to_stored(params = {})
if params[:older]
return_to = session[:return_to_older]
else
return_to = session[:return_to]
end
#test if we set return_to to something other than nil
if return_to
session[:return_to] = nil
redirect_to_url(return_to)
else
#flash[:message] = “session[:return_to] was empty, returning to list page”
redirect_to :controller => ‘article’, :action => ‘list’
end
end
And this is where i call it, from the delete_story method:
def delete
@story = Story.find(params[:id])
@article = @story.article
#before deleting, we need to check that we weren;t on the story/show page
#if we were, we can’t go back there after deleting: go to the previous instead
@story.destroy
if session[:return_to].match(/reeply\/show/)
redirect_to_stored :older => “true”
else
redirect_to_stored
end
end
This now works for deleting drafts - you go back to the “your drafts” page. But it doesn’t work for deleting stories that you got to from the article/show page now - you go back TOO FAR! agghh. I think i’m going about this in a very clumsy way. OK, tweaked it a bit - now the controller goes back to the draft_list if we came from their recently, otherwise it always goes back to article/show, for the article that owned the deleted reeply.
def delete
@story = Story.find(params[:id])
@article = @story.article
@story.destroy
#after deleting a reeply, we either go to the ‘your drafts’ page or to
#the show page for the article that had that reeply
if session[:return_to].match(/reeply\/list_drafts/) ||
session[:return_to_older].match(/reeply\/list_drafts/)
redirect_to :controller => “reeply”, :action => “list_drafts”
else
redirect_to :controller => “article”, :action => “show”, :id => @article.id
end
end
OK, next thing - i think that links to ‘edit story’ or ‘delete_story’, etc, should be buttons, in keeping with their more significant actions. There should also be buttons to ’save draft’ and ‘delete’ on the ‘edit_story’ page.
Changing links to buttons couldn’t be simpler - just ‘button_to’ instead of ‘link_to’. There seems to be a slight difference in that if you put two buttons next to each other, each inside a span element, they go on seperate lines. So i put them in a little table if i need them next to one another. So, that’s all easy.
A bit more complex is adding in the new buttons to the story/edit page: the way i did this, since all the buttons are in the middle of the form, is to have them all labelled as submit tags, and then, in the create method of the story_controller, use a case statement to analyse :commit (which has the text from the button that was pressed) to decide what to do next.
Here’s my new list of buttons on the story/edit form:
<%= submit_tag “Submit Reeply” %>
<%= submit_tag “Save changes” %>
<%= submit_tag “Delete Reeply” %>
And here’s the two new cases from story_controller/create:
when “Save changes”
if @story.save
flash[:notice] = “Saved Reeply draft.”
else
flash[:notice] = “Sorry, your Reeply draft was not able to be saved.”
end
redirect_to :controller => “story”, :action => “edit”, :id => @story
when “Delete Reeply”
redirect_to :controller => “story”, :action => “delete”, :id => @story
So, they work. Cool. Next though, i want to add confirmation dialogs to them, like i do with the other buttons to ‘Delete story’. As these are “submit_tag”, rather than “button_to”, however, it doesn’t seem to be possible.
After a bit of forum help, of course it’s possible! We just need to use a bit of javascript (of which i’m woefully ignorant). Here’s how to do it:
<%= submit_tag “Delete Reeply”,
:onclick => “return #{confirm_javascript_function(”Delete \”#{@story.title}\”?”)}” %>
Note the complicated set of brackets and quotes at the end there!