600 likes | 747 Views
Son of the Inventory App. But first. The question came up about how to run a production server with Rails…. Three Possibilities. Use fastcgi and apache Use apache as a proxy server, against a cluster of mongrel servers (this is covered in Agile Web Development with Rails)
E N D
But first • The question came up about how to run a production server with Rails…
Three Possibilities • Use fastcgi and apache • Use apache as a proxy server, against a cluster of mongrel servers (this is covered in Agile Web Development with Rails) • And the one you want, passenger:http://www.modrails.com/ • I'm still playing with this, more to come…
Installation • It's a gem so use "sudo gem install passenger" • Then run:sudo passenger-install-apache2-module • This will install the modules needed
Migrate to production • rake migrate RAILS_ENV=production • This will create a new empty database for a production server • You can copy your development db over the production one if you want to carry your test data over, or dump and load the data
Configuration • In the apache config file, check to make sure you have the modules loading • I had to add these manually on my laptop LoadModule passenger_module /usr/local/lib/ruby/gems/1.8/ gems/passenger-2.0.3/ext/apache2/mod_passenger.so # Passenger config PassengerRoot /usr/local/lib/ruby/gems/1.8/gems/passenger-2.0.3 PassengerRuby /usr/local/bin/ruby
Virtual Host • You need to define a virtual host • This is what I’m using for now • Note that I’m using podzone.net, this is dyndns • The base uri should point to a soft link to your application's public folder--doesn't work for me <VirtualHost *:80> ServerAdmin webmaster@dummy-host2.example.com DocumentRoot /Library/WebServer/Documents RailsBaseURI /inventory RailsSpawnMethod conservative ServerName avatar.podzone.net </VirtualHost>
Odds and Ends • After configuring the web server, restart it:sudo apachectl graceful • Watch your logs:tail -f /var/log/httpd/error_log
Tonight and Next Week • Tonight • time and date • tags • lots searches • Next Week • more searches • email • filesystem access • ?
When we left our application • A database backend • Some Migrations • Played with Types • Played with Time Methods • Some Validations
Let's take a look at the source • Model • Controller • Views
Next Steps • Refine status options • Refine date_submitted • Add searches
Status • We can't depend on the user to type what we want • So we need to define what statuses we'll accept • ordered, delivered, deployed • Provide a controlled method for selection • Radio buttons
new.html.erb • In the views/machine dir, we need to edit new.html.erb file • This one's easy, note we're setting a default <p> <%= f.label :status %><br /> <%= f.radio_button :status, "ordered", :checked => true %> Ordered <br/> <%= f.radio_button :status, "delivered" %>Delivered <br/> <%= f.radio_button :status, "deployed" %>Deployed <br/> </p>
edit.html.erb • For the edit view, the same thing without the default <p> <%= f.label :status %><br /> <%= f.radio_button :status, "ordered" %>Ordered <br/> <%= f.radio_button :status, "delivered" %>Delivered <br/> <%= f.radio_button :status, "deployed" %>Deployed <br/> </p>
A validation • We might not need it, but jic • Note the use of %w Check status for a valid choice validates_inclusion_of \ :status, \ :in => %w(ordered delivered deployed), \ :message => "Status must be ordered, delivered, or deployed"
date_submitted date_select datetime_select distance_of_time_in_words distance_of_time_in_words_to_now select_date select_datetime select_day select_hour select_minute select_month select_second select_time select_year time_ago_in_words time_select • The date_submitted is just too precise--we don't need the time values, just the date • We're using datetime_select, but time and date methods?
What's the difference? • date_select, datetime_select, select_time generate inputs tied to an object in the model • select_date, select_datetime, select_day, select_hour, select_minute, select_month, select_second, select_time, select_year produce inputs, but these are not tied to an object in the model • distance_of_time_in_words, distance_of_time_in_words_from_now, time_ago_in_words calculate intervals • This distinction between methods tied to the model and those which are not is pervasive
date_select • date_select give us an input for just the date • Lots of options <p> <%= f.label :date_submitted %><br /> <%= f.date_select \ :date_submitted, \ :order => [:month, :day, :year], \ :start_year => 2008 %> </p>
Another Validation • We should also refine the date_submitted validation • We don't allow selection in the past, but we should allow that for at least a week or so in the past • We should limit the future as well
First Try: 1 day ago and Err This validates that the date and time are reasonable values def date_submitted_must_be_sensible errors.add(:date_submitted, \ 'Time and date cannot be in the past')\ if date_submitted < 3600.mins.ago This one is too restrictive if date_submitted < Time.now errors.add(:date_submitted, \ 'Time and date cannot be more than 2 years in the future')\ if date_submitted > Time.now.advance(:years => 2) is equivalent to: #if date_submitted > 2.years.from_now end
This Works # This validates that the date and time are resonable values def date_submitted_must_be_sensible errors.add(:date_submitted, \ 'Time and date cannot be more than 2 months in the past')\ if date_submitted < 2.months.ago # This one is too restrictive # if date_submitted < Time.now errors.add(:date_submitted, \ 'Time and date cannot be more than 2 years in the future')\ if date_submitted > Time.now.advance(:years => 2) # is equivalent to: #if date_submitted > 2.years.from_now end
Refining Date Appearance • The inputs are good, but the display leaves a bit to be desired…
Using strftime • strftime is a method that will format a datetime object in custom format • The way it works is you call the method with a string that represents the format you want • The % sigil represents a time or date symbol <p> <b>Date submitted:</b> <%=h @machine.date_submitted.strftime "%m-%d-%Y" %> </p>
strftime formatting %a - abbreviated weekday name according to the current locale %A - full weekday name according to the current locale %b - abbreviated month name according to the current locale %B - full month name according to the current locale %c - preferred date and time representation for the current locale %C - century number (the year divided by 100 and truncated to an integer) %d - day of the month as a decimal number (range 01 to 31) %D - same as %m/%d/%y %e - day of the month as a decimal number %g - like %G, but without the century. %H - hour as a decimal number using a 24-hour clock (range 00 to 23) %I - hour as a decimal number using a 12-hour clock (range 01 to 12) %j - day of the year as a decimal number (range 001 to 366) %m - month as a decimal number (range 01 to 12) %M - minute as a decimal number %n - newline character %p - either `am' or `pm' according to the given time value %r - time in a.m. and p.m. notation %R - time in 24 hour notation %S - second as a decimal number %t - tab character %T - current time, equal to %H:%M:%S %u - weekday as a decimal number [1,7], with 1 representing Monday • Lots of options • There are a few more • I think all of these work with rails from http://us.php.net/strftime
Refining our index • Currently, the index.html.erb file returns all of the records, by virtue of @machines = Machine.find(:all) • We don't really want that, since any records exported to the main db are not longer relevant @machines = Machine.find(\ :all, \ :conditions => { :exported_to_main => false})
Searches • At this point, we have a pretty nice app • But we don't have a search tool • We'll want a simple search for the main page • Also an advanced search page with more options
Simple Search • For this we'll use a find method • We'll need an if statement in the controller • We'll also need an input on the index.html.erb file • To get started, we'll just search for a name match
The input • So far, the inputs we have are all associated with objects in the model • We don't need a db entry tho • Use an input not linked to the model, text_field_tag • Inputs independent of the model end in _tag by convention
The Input • Here's the input • Note the use of the params hash • Also that the name of the submit_tag is nil • This will default to a get <p> <%= text_field_tag :general_search, params[:search] %> <%= submit_tag "General Search", :name => nil %> </p>
An if to catch the search • We need to check if we have the get var • Then we set @machines to be the result of a find • Here, the find just looks for a match between the data passed via the get into the params hash and the user_name object in the model… if params[:general_search] @machines = Machine.find(\ :all, \ :conditions => [ "user_name = ?", params[:general_search] ]) end
find • Conditions can be used as a string, array or hash • If you use strings, you should sanitize def self.authenticate_unsafely(user_name, password) find(:first, :conditions => "user_name = '#{user_name}' AND password = '#{password}'") end def self.authenticate_safely(user_name, password) find(:first, :conditions => [ "user_name = ? AND password = ?", user_name, password ]) end def self.authenticate_safely_simply(user_name, password) find(:first, :conditions => { :user_name => user_name, :password => password }) end http://api.rubyonrails.com/classes/ActiveRecord/Base.html#M001696
find_by_sql() • You can also use sql with the find_by_sql method • This does tie you to the db backend # A simple SQL query spanning multiple tables Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] http://api.rubyonrails.com/classes/ActiveRecord/Base.html#M001696
Refining a find if params[:general_search] #@target = params[:general_search] @machines = Machine.find(:all, \ :conditions => [ \ "user_name = ? OR \ from_entity = ? OR \ from_location = ? OR \ to_entity = ? OR \ to_location = ?", \ params[:general_search], params[:general_search], params[:general_search], params[:general_search], params[:general_search] ]) end • Here, we're using an array with Ors to search the basic text fields
Combining code • Right now, this works because we're setting @machines first with a find that looks for records not exported to main, and then reset it if general_search is set in params • This is sloppy, we should combine them • So move the first find to an else
if params[:general_search] @machines = Machine.find(:all, \ :conditions => [ "user_name = ? OR from_entity = ? OR from_location = ? OR to_entity = ? OR to_location = ?", params[:general_search], params[:general_search], params[:general_search], params[:general_search], params[:general_search] ]) else @machines = Machine.find( \ :all, \ :conditions => { :exported_to_main => false}) end
One more thing :conditions => [ "user_name LIKE ? OR from_entity LIKE ? OR from_location LIKE ? OR to_entity LIKE ? OR to_location LIKE ?", "%#{params[:general_search]}%", "%#{params[:general_search]}%", "%#{params[:general_search]}%", "%#{params[:general_search]}%", "%#{params[:general_search]}%" ]) • Our search so far will only find exact matches • We can use a LIKE condition • Notice the use of % and #{}
Advanced Search • We could probably do this with just a new view • But for grins, we'll create a new controller named search with an index page: ruby script/generate controller search index
A simple page • Just to be able to test things out, we'll make a new page, ordered.html.erb in the views/search dir (which was created when we generated the controller) • This will just display some simple results, since we're just testing…
ordered.html.erb <h1>Ordered Machines</h1> <% for machine in @machines -%> <%=h machine.user_name %><br/> <%=h machine.date_submitted.strftime "%m-%d-%Y" %><br/> Ticket: <%=h machine.ticket_number %><br/> From: <%=h machine.from_entity %> To: <%=h machine.to_entity %><br/> <%=h machine.comments %><br/> <hr/> <% end %>
Try to load it • If we load the page using:http://127.0.0.1:3000/search/ordered • we get an error, no method • This is because our new controller has no methods defined
a better find • find as a method is handy, but it's limited • For rails 1x, Nick Kallen wrote a plugin, has_finder, which extended searches • This functionality was incorporated into rails 2.x as named_scope • A class method, this will create a custom method with search conditions
machine.erb • In the machine controller, we can create a named scope that finds machines that have been ordered • We can then call this from the search controller • We'll do more with named_scope next week--it's more flexible than find but also a bit confusing # Using named_scope, define a search that returns # records of machines that have been ordered named_scope :machines_ordered, :conditions => { :status => "ordered" }
search.erb • In the search controller, we then set a variable • So when we load /search/ordered, the search controller accesses the method we defined in the machine controller, and passes the data to ordered.html.erb # This structure accesses a named scope # in machines_controller.erb @machines = Machine.machines_ordered
Ok? • Yes, this is ok, but if we follow this approach through, we'll wind up with many pages • What would be better would be to use the search/index.html.erb file to allow selection, and then pass the data into the machine/index.html.erb file • So we start with the search/index.html.erb file • And we'll start with find…
search/index.html.erb • Some text_field_tags and <h2>Advanced Search</h2> <h3>Choose Field</h3> <% form_tag machines_path, :method => 'get' do %> <p> <%= text_field_tag :search_target, params[:search] %> for <%= text_field_tag :search_string, params[:search] %> <%= submit_tag "Search", :name => nil %> </p> <% end %>
search/index.html.erb • This gives us a simple form, for which we enter the field to search and the string to search for
machine controller • Add an if to the controller, this time we'll use a find with slightly different syntax @machines = Machine.find( :all, :conditions => { params[:search_target] => params[:search_string]} ) if params[:search_target]
A basic working search • So we have a basic form that has some flexibility for searches • Everything up to this point is in inventory02.gz.tar up in the samples dir
A selection • What we have works, but it's awkward • For our advanced search we need a select • We'll start with an html select <select name="search_target"> <option value="user_name">User Name</option> <option value="from_entity">From Entity</option> <option value="from_location">From Location</option> <option value="to_entity">To Entity</option> <option value="to_location">To Location</option> </select>