I'm very new to Ruby on Rails and trying to create a search function that allows the user to serach multiple parameters at the same time; from, and to. Something to keep in mind is that there will probably be even more parameters later on in the development. I've got it to work when searching for one of the fields, but not more than that.
Search view:
<%= form_tag(journeys_path, :method => "get", from: "search-form") do %>
<%= text_field_tag :search_from, params[:search_from], placeholder: "Search from", :class => 'input' %>
<%= text_field_tag :search_to, params[:search_to], placeholder: "Search to", :class => 'input' %>
<%= submit_tag "Search", :class => 'submit' %>
<% end %>
Method:
class Journey < ActiveRecord::Base
def self.search(search_from)
self.where("from_place LIKE ?", "%#{search_from}%")
end
end
Controller:
class JourneysController < ApplicationController
def index
#journeys = Journey.all
if params[:search_from]
#journeys = Journey.search(params[:search_from])
else
#journeys = Journey.all.order('created_at DESC')
end
end
def search
#journeys = Journey.search(params[:search_from])
end
end
I've tried some gems and all kind of solutions that I've found in other questions, but I'm just not good enough at RoR yet to succesfully apply them correctly without help. I would appreciate any help I can get.
Thank you!
Model:
class Journey < ActiveRecord::Base
def self.search(search_from, search_to)
self.where("from_place LIKE ? and to_place LIKE ?", "%#{search_from}%", "%#{search_to}%")
end
end
Controller:
class JourneysController < ApplicationController
def index
if params[:search_from] and params[:search_to]
#journeys = search
else
#journeys = Journey.all.order('created_at DESC')
end
end
def search
#journeys = Journey.search(params[:search_from], params[:search_to])
end
end
The best approach here is to incapsulate your search form as a separate Ruby class. Using Virtus here helps to get type coercion for free.
class SearchForm
include Virtus.model # Our virtus module
include ActiveModel::Model # To get ActiveRecord-like behaviour for free.
attribute :from, String
attribute :to, String
# Just to check if any search param present,
# you could substitute this with validations and just call valid?
def present?
attributes.values.any?{|value| value.present?}
end
```
In Rails 3 IIRC you also have to include ActiveModel::Validations to be able to validate your form input if needed.
Now, let's see how to refactor controller. We instantiate form object from params and pass that to the model query method to fetch records needed. I also moved ordering out of if clause and used symbol ordering param - cleaner IMO.
def index
#search_form = SearchForm.new(search_params)
if #search_form.valid? && #search_form.present?
#journeys = Journey.search(#search_form)
else
#journeys = Journey.all
end
#journeys = #journeys.order(created_at: :desc)
end
def search
#journeys = Journey.search(SearchForm.new(search_params)
end
private
def search_params
params.require(:search_form).permit(:from, :to)
end
Now to the view: form_for will work perfectly with our form object, as will simple_form_for
<%= form_for #search_form, url: journeys_path, method: :get do |f| %>
<%= f.text_field :from, placeholder: "Search from", class: 'input' %>
<%= f.text_field :to, placeholder: "Search to", class: 'input' %>
<%= f.submit "Search", class: 'submit' %>
<% end %>
View looks now much shorter and cleaner. Incapsulating params in object makes working with search params muuuuch easier.
Model:
class Journey < ActiveRecord::Base
def self.search(search_form)
if search_form.kind_of?(SearchForm)
journeys = all # I'm calling Journey.all here to build ActiveRecord::Relation object
if search_form.from.present?
journeys = journeys.where("from_place LIKE ?", "%#{search_form.from}%")
end
if search_form.to.present?
journeys = journeys.where("to_place LIKE ?", "%#{search_form.to}%")
end
else
raise ArgumentError, 'You should pass SearchForm instance to Journey.search method'
end
end
end
Notice how I build ActiveRecord::Relation object by calling Journeys.all and applying each search param if present. Chaining where like that would put AND in between automatically, if you need OR Rails 4 has it: Journey.or(condition).
Pros of this approach:
You are using Plain Old Ruby Classes, almost no magic here, and it works like usual Rails model in many ways. Putting search params in the object makes it a lot easier to refactor code. Virtus is the only dependency, sans Rails itself of course, and it's more for convenience and to avoid writing boring boiler-plate code.
You can easily validate input if needed (If you really want to be strict about input and show user validation error instead of silently executing stupid query with contradicting conditions and returning no results).
Related
I want to have two input fields, but only one column in the database.
The first input is stored data in numbers and the other one is stored data in numbers divided by 24.
You can put data only in one field.
Is there any possible way to do this?
UPD:
Migration:
def change
add_column :employees, :hourly_payment , :integer
end
View:
employees/_form.html.erb
<%= simple_form_for #employee do |form| %>
<%= form.input :name %>
<%= form.input :hourly_payment %>
<%= form.input :monthly_payment %>
<%= form.button :submit, class: "btn btn-success" %>
<% end %>
Your model and database tables are the internals of your application and are not actually tied to the view by anything except how easy ActiveRecord makes it to use convention over configuration to link the two*.
In Rails style MVC the controller is responsible for passing user input to the model. Usually you would just do this with simple mass assignment:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
# ...
end
private
def user_params
params.require(:user)
.permit(:email, :salary)
end
end
This is basically just passing a whitelisted hash of parameters straight to the model as is and it all gets passed to the setters that ActiveRecord magically created for you by reading the database schema.
But there is nothing stopping you from assigning attributes manually:
class UsersController < ApplicationController
def create
#user = User.new(user_params) do |user|
user.salary = calculated_salary
end
# ...
end
private
def user_params
params.require(:user)
.permit(:email)
end
def calculated_salary
if params[:user][:hourly_payment].present?
params[:user][:hourly_payment]
elsif params[:user][:monthly_payment].present?
params[:user][:monthly_payment].to_i / 168
else
0 # sorry no cookies for you
end
end
end
Or monkeying with the parameters object:
def user_params
params.require(:user)
.permit(:email)
.merge(salary: calculated_salary)
end
It is after all just a hash on steroids. The only thing that Rails will prevent you from is passing a parameters object that has not been whitelisted.
There is no stone tablet for what you can do in a controller. The only thing to bear in mind is that controllers are notoriously hard to test and fat controllers are a recipe for disaster.
If you're doing anything more complicated there are better solutions such as form objects, decorators or service objects.
You'll need to create a view for that. Here is an example of migration:
def up
sql = %(CREATE OR REPLACE VIEW my_view_models AS
SELECT m.*,m.int_field*12 as my_new_value from my_models m
)
self.connection.execute(sql)
end
def down
self.connection.execute('DROP VIEW IF EXISTS my_view_models')
end
Then, you can access your value with method my_new_value on your model. You'll need to change the name of the default table matching your model.
class MyModel
self.table_name = 'my_view_models'
end
And access it via
MyModel.first.my_new_value
I am learning Ruby on Rails and have a search form set up and its working. On the pins index view I can search for pins(posts) by their title. However if I wanted to search by Username which is not in the pins table and display the results on the Pins index page how would I do this? How do I access an attribute from a different table? (Sorry for the newbie attempt at explaining my issue)
Pins controller
def index
#pins = Pin.search(params[:term])
end
Pin Model
def self.search(term)
if term
where('description LIKE ?', "%#{term}%")
else
order('id DESC')
end
end
_search.html.erb
<%= form_tag(pins_path, method: :get) do %>
<%= text_field_tag :term, params[:term] %>
<%= submit_tag 'Search', description: nil %>
<% end %>
Assuming you have set up something like
class User < ApplicationRecord
has_many :pins
# the username is stored in the attribute 'username'
end
class Pin < ApplicationRecord
belongs_to :user
end
you may do the following
# PinsController
def index
terms = params[:term]
username = params[:username]
#pins = Pin
#pins = #pins.where("description LIKE '%?%'", term) if term
#pins = #pins.includes(:user).where("users.username LIKE '%?%'", username) if username
# you may want to sort by id anyway
#pins = #pins.order('id DESC')
end
Note that I put the code straight to the controller for brevity. You may refactor this to use your search method in pin model.
# _search.html.erb
<%= form_tag(pins_path, method: :get) do %>
<%= text_field_tag :term, params[:term] %>
<%= text_field_tag :username, params[:username] %>
<%= submit_tag 'Search', description: nil %>
<% end %>
In case you want to do some more searching and filtering you may have a look at the ransack gem although I think you're going the right path in trying to figure this out yourself.
Although those railscasts episodes are from the past I think they are applicable to the current rails versions. Anyway one can get the point from them
http://railscasts.com/episodes/37-simple-search-form
http://railscasts.com/episodes/111-advanced-search-form
http://railscasts.com/episodes/240-search-sort-paginate-with-ajax
Another good resource is gorails.com (not affiliate in any way!!). I can highly recommend them as a resource for learning
Provided you have an association between User and Pin
class User
has_many :pins
end
class Pin
belongs_to :user
end
You can join :user from Pin and set conditions on the association:
Pin.joins(:user).where('users.username ? AND awesome = ?', 'Max', true)
# or the preferred method
Pin.joins(:user).where(user: { username: 'Max', awesome: true })
Note that we use users.username and not user.username when writing a SQL string you're specifying the table name - not the association.
To search for pins based on the username you could do:
def self.by_username(term)
joins(:user).where('users.username LIKE ?', "%#{term}%")
end
I am currently trying to make custom validations work with an input of dates, but, unfortunately, it doesn't seem to work.
There are two pages inside the application, Index page and Search page. Inside the index page there is a text field that takes in a date. I am using Chronic gem which parses text into dates. If the date is invalid, Chronic returns nil. If it is valid, it redirects to search page and shows the date.
The code I wrote so far doesn't seem to work properly, but what I want to achieve is..
1) to validate that Chronic doesn't return nil
2) to validate that date is greater than today's date
Please note that I am not using a database with this, I just want to be able to validate inputted date without ActiveRecord. If someone could help me with this, your help will be greatly appreciated.
views/main/index.html.erb
<%= form_tag({controller: "main", action: "search"}, method: "get") do %>
<%= label_tag(:q, "Enter Date:") %>
<%= text_field_tag(:q) %>
<%= submit_tag "Explore", name: nil %>
<% end %>
views/main/search.html.erb
<%= #show_date %>
main_controller.rb
def search
passed_info = Main.new
if passed_info.valid_date?
#show_date = passed_info
else
flash[:error] = "Please enter correct date!"
render :index => 'new'
end
end
models/main.rb
class Main
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :q
validates_presence_of :q
def initialize(params={})
params.each do |attr, value|
self.public_send("#{attr}=", value)
end if params
end
def persisted?
false
end
def valid_date?
require 'chronic'
if Chronic.parse(q).nil? || Chronic.parse(q) < Time.today
errors.add(:q, "is missing or invalid")
end
end
end
EDIT:
this is what goes wrong...
localhost:3000
then it redirects to ..
localhost:3000/main/search?utf8=%E2%9C%93&q=invalid+date+test
No validation, no date, nothing..
The Problem
Be more careful with return values. When you try to guard your controller with if valid_date?, what you're doing is checking to see if valid_date? returns false. If the parse fails, the return value is the output of errors.add, which in turn is the output of Array#<<. Relevantly, the output isn't nil or false, so it evaluates to true, thus the if clause passes and you move forward.
Potential Solution
You probably want to let the Rails Validation Framework do more work for you. Instead of treating valid_date? as a public method which the controller calls, call the valid? method that gets added by ActiveModel::Validations. valid? will return a boolean, based on whether all the model validations pass. Thus, you would, as is the Rails Way, call if model_instance.valid? in your controller.
This lets you just write validator methods in your model which express the logic you're trying to write. Right now, you have all validation logic for dates in a single method, with a single error message. Instead, you could put two methods, which add more descriptive individual error methods.
class YourClass
include ActiveModel::Validations
validate :date_is_valid
validate :date_not_before_today
private
def date_is_valid
if Chronic.parse(q).nil?
errors.add(:q, "Date is invalid")
end
end
def date_not_before_today
if Chronic.parse(q) < Date.today
errors.add(:q, "Date cannot be before today")
end
end
end
Just as correctly suggested by ABMagil, I would like to post the full solution to my answer. In fact, this answer can apply really to anyone who wants to use validations using ActiveModel, with or without Chronic gem or dates involved. It can act as a valid template so to speak.
Frankly, most of my mistakes came from a really poor, at the time, understanding of what I actually tried to achieve. Most of the code needed major refactoring, see below the updates that I had to make. I tried to keep the code as well documented as possible.
Solution:
views/main/index.html.erb
<%= form_for #search, url: { action: "search" },
html: { method: :get } do |f| %>
# Displays error messages if there are any.
<% if #search.errors.any? %>
The form contains <%= pluralize(#search.errors.count, "error") %>.<br />
<% #search.errors.each do |attr, msg| %>
<%= msg %><br />
<% end %>
<% end %>
<%= f.label :q, "Enter Date:" %>
<%= f.text_field :q %>
<%= f.submit "Explore", :class => 'submit' %>
<% end %>
views/main/search.html.erb - same as before
<%= #show_date %>
main_controller.rb
def index
# Initializes a string from the form to a variable.
#search = Search.new
end
def search
# Retrieves the input from the form.
#search = Search.new(params[:search])
# Checks for validity,
# If valid, converts a valid string into a date.
# Redirects to search.html.erb
# If not valid, renders a new index.html.erb template.
if #search.valid?
#show_date = (Chronic.parse(params[:search][:q])).to_date
else
render :action => 'index'
end
end
models/main.rb
class Main
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
# Accepts the passed attribute from the form.
attr_accessor :q
# If form is submitted blank, then output the prompting message.
validates_presence_of :q, :message => "This text field can't be blank!"
# Two custom validations that check if passed string converts to a valid date.
validate :date_is_valid
validate :date_not_before_today
# Initializes the attributes from the form.
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
# Checks for persistence, i.e. if it's a new record and it wasn't destroyed.
# Otherwise returns false.
def persisted?
false
end
# ABMagil's code used for custom date validations
private
require 'chronic'
def date_is_valid
if Chronic.parse(q).nil?
errors.add(:base, "Date is invalid")
end
end
def date_not_before_today
if !Chronic.parse(q).nil?
if Chronic.parse(q) < Date.today
errors.add(:base, "Date cannot be before today")
end
end
end
end
Result:
I am trying to generalize few methods that will be used by multiple models/views/controllers but i'm having no luck. Here is the original code that works when it is just for 1 set called Trucks.
View
<h2>Trucks</h2>
<%= form_tag trucks_path, :method => 'get' do %>
<%= hidden_field_tag :direction, params[:direction] %>
<%= hidden_field_tag :sort, params[:sort] %>
<p>
Search:
<%= text_field_tag :search %>
by
<%= select_tag :search_column, options_for_select(Truck.translated_searchable_columns(['attribute1']), params[:search_column]) %>
<%= submit_tag "Search" %>
</p>
<% end %>
<!-- Display code goes here, but im not showing since its just a table -->
Controller
def index
#trucks = Truck.search(params[:search], params[:search_column]).order(sort_column(Truck, "truck_no") + " " + sort_direction)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #trucks }
end
end
Model
class Truck < ActiveRecord::Base
attr_accessible :attribute1, :attribute2, :attribute3
def self.search(keyword, column_name)
if self.column_names.include?(column_name.to_s)
where("trucks.#{column_name} LIKE ?", "%#{keyword}%")
else
scoped
end
end
def self.searchable_columns(unwanted_columns)
self.column_names.reject{ |column| unwanted_columns.include?(column) }
end
def self.translated_searchable_columns(unwanted_columns)
columns = self.searchable_columns(unwanted_columns)
result = columns.map{ |column| [Truck.human_attribute_name(column.to_sym), column] }
result
end
end
All this works without a hitch, now I can't figure out for the life of me how to move these methods to lib and have them generalized so that lets say Trailers is able to call in the same method and pass in its information and achieve the same result. I am trying to make this code DRY as possible. Could anyone explain me what I need to do to achieve this? How does lib access the database?
The concept you're looking for is called a "concern". Rails has a convenience module for implementing concerns called ActiveSupport::Concern. Here's how you might extract your model methods:
module Searchable
extend ActiveSupport::Concern
module ClassMethods
def search(keyword, column_name)
if column_names.include?(column_name.to_s)
where("#{table_name}.#{column_name} LIKE ?", "%#{keyword}%")
else
scoped
end
end
def searchable_columns(unwanted_columns)
column_names.reject{ |column| unwanted_columns.include?(column) }
end
def translated_searchable_columns(unwanted_columns)
columns = searchable_columns(unwanted_columns)
columns.map{ |column| [human_attribute_name(column.to_sym), column] }
end
end
end
And then in your model:
class Truck < ActiveRecord::Base
include Searchable
attr_accessible :attribute1, :attribute2, :attribute3
end
As for where exactly you should store the Searchable module, it's up to you -- it just has to be someplace that's included in config.autoload_paths, just like a model or controller. Rails 4 introduced a convention that model concerns are stored in app/models/concerns, and controller concerns in app/controllers/concerns, but there is nothing special about these locations other than being autoloaded by default.
I am looking to implement a simple search function while using the globalize3 gem for Ruby on Rails. Since the translations of the model are stored in a separate table, the code below doesn't work as there is no longer a :name field in the products table. How can I adjust the code below to make the search function correctly?
products_controller.rb
#products = Product.search(params[:search]).all
index.html.erb
<%= form_tag products_path, method: :get do %>
<%= text_field_tag :search, params[:search] %>
<%= submit_tag "Search", name: nil %>
<% end %>
model
class Product < ActiveRecord::Base
translates :name
attr_accessible :name, :price, :released_at
def self.search(search)
if search
where('name LIKE ?', "%#{search}%")
else
scoped
end
end
end
You're in luck, I tackled exactly the same problem recently!
Luckly for you the answer is quite simple. You can use the class method with_translations to include translations for a given set of locales.
Here's the code:
def with_translations(*locales)
locales = translated_locales if locales.empty?
includes(:translations).with_locales(locales).with_required_attributes
end
Include it in your search method:
def self.search(search)
if search
with_translations.where('name LIKE ?', "%#{search}%")
else
with_translations
end
end
That should do it.
As an added note: you could add an optional locales parameter to the search method and pass it to with_translations to optionally narrow the search to terms in a particular language, say for example in the current locale.
Solved ...
def index
if params[:search]
#at = []
#at = Array.new
Article.translation_class.where("title LIKE ? OR description LIKE ?","%#{params[:search]}%","%#{params[:search]}%").all.each do |t|
#at.push t.article_id
end
#articles = Article.where(id: #at).recent_first.paginate(page: params[:page], per_page: 5)
else
#articles = Article.all.recent_first.paginate(page: params[:page], per_page: 5)
end
end