How to implement database search like this with Rails? - ruby-on-rails

I am working on my first website using Ruby on Rails. The idea is a nutrition "search engine", where you search for a food and then the web app takes you to either a page with nutrition information about that food or a page of search results if no exact match exists.
So far, I have created the nutrition information database (using Postgresql) and added some test data. This is the command I used to create the scaffolding for the database:
rails generate scaffold Food name:string calories:decimal portion_name:string \
portion:decimal image_url:string description:text
I then created the index page. This is the command I used to create the index:
rails generate controller home index
I removed public/index.html and modified config/routes.rb to contain this line:
root :to => 'home#index'
After this, I added the search form to app/views/home/index.html.erb, like so:
<div id="search">
<h1>How many calories are in... ?</h1>
<%= form_tag("/search", :method => "get") do %>
<%= text_field_tag "q", nil, :class => "span6" %>
<%= submit_tag("Go", :name => nil) %>
<% end %>
</div>
Now, I would like to hook up this search form so that, if there is an exact match to the search query or one near match, the user is routed directly to a page displaying information (from the database) for that food. If there isn't an exact match, I would like the user to be presented with a page of results and links to the nutrition page for each result.
How can I implement this functionality? I have tried googling for articles about implementing search with Rails and I have watched a screencast, also, but none of the resources I have found are implementing quite what I want.
Here's a GitHub link to the project so far: https://github.com/robertseaton/nutrition-project.

I use Thinking-Sphinx for searching in Rails - Sphinx is an open source search server. You can define how you want your search to work right in Rails.

You can also use sunspot to cater to your needs of the search. Here is the github link of sunspot https://github.com/sunspot/sunspot .
Using sunspot you can specify what all items are going to be searchable like food, calories etc. and also give various weights to these items so that if there is a keyword match lets say between food and protien which result should be displayed at the top.
sunspot wiki at github has got lot of resources to get you started and it is very intuitive to implement :).
Thanks,

Related

Best way to do CSV seed file upload to rails repo through administrate?

The main table in my backend gets populated by a CSV file. When seeding and running rails db:seed , the CSV files in lib/assets/csv is read with file = File.read(Rails.root.join('lib', 'assets', 'csv', 'data.csv'), and the logic in seeds.rb runs through each row to create a table entry, populating the fields with the CSV column data.
I've also implemented Thoughtbot's Administrate for a UI admin dashboard to view this data.
So my question is, what is the best way to configure some kind of custom file upload system on Administrate dashboard if I ever need to replace the files sitting in lib/assets/csv and reseed?
I've looked at ActiveStorage but I've only ever used it to store files like an image specifically related to a table entry, not for seeding the entire table.
Yes, it is possible, and you don't need ActiveStorage. Rails's basic file upload facilities will be enough. Meanwhile, Administrate tries to follow Rails conventions, and provides hooks for you to alter the templates and refer to your own controllers and actions where you can implement this. Here's an example of how you could solve your problem.
First, you'll want to add a form with a file field. Admins will be able to use it to upload CSVs.
There are many places where you could put this form. For this example, let's say that it's "products" that you want to import (model Product), and you want to put the link in the index page.
You can override Administrate's own templates with your own ones. The following command will generate a copy of the index template that you can customize and Administrate will use instead of its own:
$ ./bin/rails g administrate:views:index
This will get you a new file app/views/admin/application/index.html.erb.
Alter it to add a link to a separate page that will host the upload form:
<% if page.resource_name == "product" %>
<div class="link-to-upload"><%= link_to "CSV Import", upload_admin_products_path %></div>
<% end %>
You can put this in between the page title and the search bar. Or wherever you want, really. It does the following:
The if checks that we are in the index page for "products" and not for something else.
The link_to links to a page that we haven't created yet, which will host the upload form.
If you load the products index page now, it will show an error NameError, with message undefined local variable or method 'upload_admin_products_path'. This is because we still don't have a route for this new page of ours. Lets add it now.
In the routes file config/routes.rb, you'll already have a route resources :products. Change it to look like this:
resources :products do
collection do
get :upload, action: 'upload_form'
post :upload, action: 'upload_process'
end
end
This actually adds two routes, not one. One will be for the form page, while the other one will be for the action that will process the uploaded CSV.
For now, you can click on the link and go to the new page. It will break again. This time the error will be Unknown action - The action 'upload_form' could not be found for Admin::ProductsController. That's because we haven't provided a view for this page yet. Let's do it now.
Add a view with the following contents at app/views/admin/products/upload_form.html.erb:
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
Import products from CSV
</h1>
</header>
<section class="main-content__body main-content__body">
<%= form_for :products, html: { class: "form" } do |f| %>
<p><%= f.file_field(:file) %></p>
<p><%= f.submit "Upload" %></p>
<% end %>
</section>
Now you should be able to load the page and see the form. I have added additional markup (like those header and section elements) to make it look similar to other pages that Administrate provides.
That form has a file_field and a submit button. On submit, it will send the file to the current URL (/admin/products/upload) as a POST request. It will be handled by the post route we added earlier, on the action upload_process. Let's write that now.
On the controller Admin::ProductsController, add the following method to implement the action:
def upload_process
file = params[:products][:file]
data = CSV.parse(file.to_io, headers: true, encoding: 'utf8')
# Start code to handle CSV data
ActiveRecord::Base.transaction do
data.each do |row|
Product.create(row.to_h)
end
end
# End code to handle CSV data
redirect_to admin_products_url
end
This will create new products based on what's contained in the CSV, and then will redirect the user back to the products index page.
This example action is very simple and may not suit your needs exactly. You probably want to copy your code from seeds.rb and paste it in between my Start and End comments, so that it does what you want exactly. There are many possibilities.

How to define a link path

I'm trying to figure out how to make an app with Rails 4. I keep getting stuck on basic things and I don't seem to be able to identify principles to use going forward.
I have a profile model and a industry model. The associations are:
Profile:
has_and_belongs_to_many :industries, join_table: 'industries_profiles'
Industry:
has_and_belongs_to_many :profiles, join_table: 'industries_profiles'
In my profile show page, I'm now trying to link to the industry page:
<% #profile.industries.limit(5).each do |industry| %>
<%= link_to industry.sector.upcase, industry_path(#industry) %>
<% end %>
I can't find anything that works for this link.
I have tried the following:
industry_path(#profile.industry)
industry_path(#profile.industry_id)
industry_path(industry)
industry_path(profile.industry)
industry_path(industry.id)
industry_path(industry_id)
But all of them are guesses. I don't know how to ready the API dock so I can't understand any of its content.
Can anyone see how to link to a show page of the other side of the HABTM association for a single record?
You can grab a list of your routes by running rake routes | grep industry in your command line, which will give you a table with the prefix, action, and uri pattern. For example:
industries GET /industries(.:format) industries#index
POST /industries(.:format) industries#create
new_industry GET /industries/new(.:format) industries#new
edit_industry GET /industries/:id/edit(.:format) industries#edit
industry GET /industries/:id(.:format) industries#show
PATCH /industries/:id(.:format) industries#update
PUT /industries/:id(.:format) industries#update
DELETE /industries/:id(.:format) industries#destroy
In your case, you should look at the show path. Which is industry and you append _path to the end of whatever your prefix is above, which comes out to be industry_path. And since you have declared your variable industry when defining your loop, you can use that instead of the instance variable.
Short answer: industry_path(industry)

How do I take this from the console to a controller and view

I'm learning Ruby on Rails (my first MVC) and have successfully setup a many-to-many relationship between "Agents" and "Zipcodes." What I'm trying to do currently is to get the associated agent based on the zip code entered by the user. I'm able to so successfully in the console, but am having a difficult time translating it to a controller and view.
What I do in the console:
zip = Zipcode.find_by_zip(gets.chomp)
=> 92562
zip.agents
The hangup I'm having is how to translate this into an action that I can access from a view with a form.
I've started by defining the action (agents#find), but am stumped as to whether this is correct and what comes after it.
def find
#user_zip = Zipcode.find_by_zip(params[:zip])
end
Hopefully someone awesome in here can point a n00b in the right direction.
When just starting with rails, I'd suggest avoiding custom actions like #find as much as possible, and instead sticking to the "Big 7" RESTful routes. Rails is really smooth when you work with it and do what it expects.
It sounds like maybe you're trying to identify a given zipcode, and then list all the agents associated with it. That sounds like a #show on Zipcode.
config/routes.rb
resources :zipcodes
app/controllers/zipcodes_controller.rb
ZipcodesController < ApplicationController
def show
#zipcode = Zipcode.find_by_zip(params[:id])
end
end
app/views/zipcodes/show.html.erb
<div>
<p>This zipcode has the following agents:</p>
<ul>
<%= #zipcode.agents.each do |agent| %>
<li>Agent #<%= agent.id %></li>
<% end %>
</ul>
</div>
You can see this page by browsing to /zipcodes/[zip].
Just put #user_zip = Zipcode.find_by_zip(params[:zip]) in the controller instead of the model.
In the view you will be able to call #user_zip.
Welcome to Rails! I recently started learning Rails as well, but I think I can help: A controller action will redirect by default to a view with the same name. So after assigning the value of #user_zip, the controller will serve up agents/find.html.erb, which will have access to #user_zip. Since #user_zip is an instance of Zipcode, you'd be able to use #user_zip.agents.
Somewhat tangential, but I also suggest considering using search rather than find for the action name, only because find and its variations are used elsewhere in Rails.

Nested Ressources in Ruby on Rails

It's actually supposed to be very trivial, but I'm experiencing strange issues.
I'm developing a Game Database, where Users can chart Game Entries and also write reviews to those reviews. Of course a Reviews URL (the show view) must be dependent of the game's id, like localhost:3000/games/1/reviews/2
So these are my routes
resources :games do
resources :reviews
end
And I want to open the show-view of a review with
<%= link_to "zum Review", game_review_path(#game, #review) %>
this is all basic tutorial stuff.
But I only recieve a Routing error. Trying instead this approach
url_for([#game, #review])
At least won't result in a routing error, but Not the reviews show-view is opened, but the game's show view, which is where this link is actually placed!
My models associations are set corretly and my controllers actions are all basic tutorial stuff. What am I doing wrong?
In your erb file try passing the local variable in the left argument and the name of the nested model in the field to the right. If you use 2 local variables you will get a routing error. #game is the local variable and review is nested under games so the below code should fix your issue.
Try:
<%= link_to "zum Review", game_review_path(#game, review) %>

ActiveAdmin - generate link to index, with filter preset?

In an ActiveAdmin page, I would like to include a link to a list of related resources. For example, given that a
Site has_many Sections and,
Section belongs_to a Site (in my ActiveRecord models),
I would like my Site's show page to include a link to Sections within the site, which would go to the Section index page, with the Site filter preset.
Note that
I do not want to use ActiveAdmin's belongs_to function;
I don't want nested resources for a number of reasons (depth of nesting > 2, as well as usability concerns).
What I want is to generate a URL similar to the one ActiveAdmin generates if I first go to the Sections index page and then filter by Site.
The query parameter list generated by ActiveAdmin's filtering feature is pretty crazy; is there a helper method I could use to achieve this goal?
Thanks!
I use this syntax:
link_to "Section", admin_sections_path(q: { site_id_eq: site.id })
I worked out a reasonably satisfactory solution after poking around in meta_search for a bit. Syntax is a bit clunky, but it does the trick.
index do
...
column "Sections" do |site|
link_to "Sections (#{site.sections.count})", :controller => "sections", :action => "index", 'q[site_id_eq]' => "#{site.id}".html_safe
end
end
As jgshurts pointed out, the trick is identifying that q[site_id_eq] query parameter.
However, if you don't like the clunky syntax, you can also just use a path helper:
link_to "Sections (#{site.sections.count})", admin_sections_path('q[site_id_eq]' => site.id)
The UrlHelper#link_to documentation shows additional examples of this.
#auto_link(resource, content = display_name(resource)) ⇒ Object
Automatically links objects to their resource controllers. If the
resource has not been registered, a string representation of the
object is returned.
The default content in the link is returned from
ActiveAdmin::ViewHelpers::DisplayHelper#display_name
You can pass in the content to display
eg: auto_link(#post, "My Link")
ActiveAdmin.register Girl do
index do
selectable_column
column :name do |girl|
auto_link(girl, girl.name)
end
column :email
column :created_at
actions
end
Useful-link: http://www.rubydoc.info/github/gregbell/active_admin/ActiveAdmin/ViewHelpers/AutoLinkHelper
Note: This is tested with ActiveAdmin (v1.1.0 and 2.0.0.alpha)
Hope this works with other version as well. Please update this answer if you are sure it works with other versions you know.

Resources