I have a project with a search feature. I have three classes, Product, Review and User. I have a self.search method in the product model doing a query on all three class tables.
When I put together the search view, It listed the product name, the review author and any text in a review that matches the search string.
Then I thought, lets pretty that up a bit.
So I'm cross referencing the ids to also list user avatars and time stamps and star ratings. But to do that, I'm making local variables searching in all the classes.
So I think there's now quite a code smell here. There is no controller to load up with #users = User.all. I'm sticking with _user = User.find_by types of local variable assignment. The search fingers are now in all class pies.
So my question is really, should my bloated search now rightly to be scaffolded into an actual class? Or is there another approach like a helper? Is it possible this is not that bad of an approach?
Ruby 2.6.5
Rails 5.2.4
Here is my view:
<div class="home-container">
<div class="welcome-sub-header">
<h1>Search Results</h1>
<div class='search-flex-container'>
<h2>Products that Match</h2>
<% if #results_products.empty?%>
<div class='search-noresult-card'>
<p>No products matched the search.</p>
</div>
<% end %>
<% #results_products.each do |results_product| %>
<div class='search-product-card'>
<div class='search-product-flex'>
<%= render partial: "shared/product_image", locals: { product: results_product } %>
<%= link_to results_product.name, product_path(results_product.id) %>
<p>Imported From <%= results_product.country %></p>
<div class="search-adjust-stars">
<%= render partial: "shared/review_stars", locals: { review: results_product.average_review } %>
</div>
</div>
</div>
<% end %>
<%= will_paginate #results_products %>
<h2>Product Reviews that Match</h2>
<% if #results_reviewers.empty?%>
<div class='search-noresult-card'>
<p>No reviewers matched the search.</p>
</div>
<% end %>
<% #results_reviewers.each do |results_reviewer| %>
<div class='search-review-card'>
<div class='search-review-flex'>
<% _reviewed_product = Product.find_by(id: results_reviewer.product_id) %>
<% _user = User.find_by(id: results_reviewer.user_id) %>
<div class="search-review-box1">
<%= render partial: "shared/avatar", locals: { user: _user } %>
<%= link_to (results_reviewer.author + " review of " + _reviewed_product.name), review_path(id: results_reviewer, product_id: results_reviewer.product_id) %><br>
</div>
<div class="search-review-box2">
<%= render partial: "shared/review_stars", locals: { review: results_reviewer.rating } %>
</div>
<div class="search-review-box3">
<p>On <%= results_reviewer.created_at.strftime('%m-%d-%Y') %></p>
</div>
<div class="search-review-box5">
<%= render partial: "shared/product_image", locals: { product: _reviewed_product } %>
</div>
</div>
</div>
<% end %>
<%= will_paginate #results_reviewers %>
<h2>Review Text that Matches</h2>
<% if #results_reviews.empty?%>
<div class='search-noresult-card'>
<p>No review text matched the search.</p>
</div>
<% end %>
<% #results_reviews.each do |results_review| %>
<% _user = User.find_by(id: results_review.user_id) %>
<div class='search-review-card'>
<%= render partial: "shared/avatar", locals: { user: _user } %>
<% _reviewed_product = Product.find_by(id: results_review.product_id) %>
<%= link_to (results_review.author + " review of " + _reviewed_product.name), review_path(id: results_review, product_id: results_review.product_id) %>
<p>Reviewed on <%= results_review.created_at.strftime('%m-%d-%Y') %></p>
<div class="search-review-text">
<%= results_review.content_body %>
</div>
</div>
<% end %>
<%= will_paginate #results_reviews %>
</div>
</div>
<div>Font made from oNline Web Fontsis licensed by CC BY 3.0</div>
</div>
This is the Product model with the self.search method.
class Product < ApplicationRecord
has_many :reviews, dependent: :destroy
validates :name, presence: true
validates_length_of :name, maximum: 30
validates :price, presence: true
validates_length_of :price, maximum: 8
validates :country, presence: true
validates_length_of :country, maximum: 50
has_one_attached :product_photo
before_save(:titleize_product)
scope :most_reviewed, -> {(
select("products.id, products.name, products.average_review,products.price, products.country, count(reviews.id) as reviews_count")
.joins(:reviews)
.group("products.id")
.order("reviews_count DESC")
.limit(6)
)}
scope :newest_product, -> { order(created_at: :desc).limit(6) }
scope :highest_reviewed, -> {(
select("products.id, products.name, products.price, products.country, products.average_review as average_review")
.joins(:reviews)
.group("products.id")
.order("average_review DESC")
.limit(6)
)}
def self.search(search)
where("lower(reviews.author) LIKE :search OR lower(products.name) LIKE :search OR lower(reviews.content_body) LIKE :search", search: "%#{search.downcase}%").uniq
end
def next
Product.where("id > ?", id).order("id ASC").first || Product.first
end
def previous
Product.where("id < ?", id).order("id DESC").first || Product.last
end
private
def titleize_product
self.name = self.name.titleize
self.country = self.country.titleize
end
end
I think you should separate search method into searchable module, and allow searchable-models declare what attributes involve in search query.
module Searchable
extend ActiveSupport::Concern
included do
#search_clauses ||= Hash.new
end
module ClassMethods
def search(key_search, scope: 'default')
search_clause ||= search_string_for(scope)
relation = joins(*search_clause[:joins].map(&:to_sym)) unless search_clause[:joins].blank?
(relation || self).where(search_clause[:where], key: "%#{key_search.downcase}%")
end
def search_clause(clause_str, scope: 'default')
#search_clauses[scope] = parse(clause_str)
# we can add dynamic function like seach_`scope_name`
end
def search_attributes(attributes, scope: 'default')
search_clause(attributes.map { |attribute| "%#{attribute}%"}.join(" OR "), scope: scope)
end
private
def search_string_for(scope)
#search_clauses&.dig(scope)
end
def parse(clause)
join_tables = Set.new
where_clause = clause.dup
parse_tables(clause) do |table|
join_tables << table unless table == self.class_name.tableize || table.blank?
where_clause.sub!("#{table}.", "#{table.tableize}.")
end
parse_logics!(where_clause)
parse_matches!(where_clause)
# other cases ...
{
joins: join_tables,
where: where_clause
}
end
def parse_logics!(clause)
clause.gsub!(/\|/, "OR")
clause.gsub!(/\&/, "AND")
# other ...
end
def parse_matches!(clause)
clause.scan(/(?<=%)[^\s\|\&\z\Z]*(?=%)/).each do |attribute|
clause.sub!(/%[^\s\|\&\z\Z]*%/, "lower(#{attribute}) LIKE :key")
end
end
def parse_tables(clause, &block)
clause.scan(/(?<=[%\s])[^%\s\|\&\z]*(?=\.)/).each(&block)
end
end
end
Example in my practice project:
class Task < ApplicationRecord
include Searchable
belongs_to :requirement
has_many :skills
# declare search clause, we can set the whole where-clause here
# those syntax %,|,& ... just for fun (simple way)
search_clause "%content% | %requirement.description%"
# with scope
search_clause "(%content% | %requirement.description%) & status = 0", scope: :open
# declare attributes
search_attributes %w[content skills.name], scope: :skill
end
# Demo
Task.search("setup") # default scope
Task.search("setup", scope: :open)
Task.search("ruby", scope: :skill)
Note: I recommend you use search-gems such as ransack, pg_search (in case of full-text-search).
Related
I want to display belongs_to relationship columns in ransackable attributes list. So that I can display them in the dropdown, and perform an advanced search on the (joined) table.
How can I do that?
Below my model, where each manifest has one consignee. I've adjusted the attribute list, but when I select the consignee name it looks for 'manifest'.'name' and not in 'consignee'.'name' via a JOIN.
When I use the simple search form, it works correctly.
manifest.rb
class Manifest < ApplicationRecord
belongs_to :shipper
belongs_to :consignee
...
def self.ransackable_attributes(auth_object = nil)
super - ['id', 'created_at', 'updated_at', 'consignee_id']
super + ['consignee_name']
end
end
consignee.rb
class Consignee < ApplicationRecord
has_many :manifest, dependent: :destroy
...
end
manifest_controller.rb
...
def index
#search = ransack_params
#search.build_grouping unless #search.groupings.any?
#manifests = #search.result(distinct: true)
#search.build_condition
...
private
def ransack_params
Manifest.includes(:vessel, :pod, :pol, :por, :del, :consignee).ransack(params[:q])
end
end
index.html.erb
<%= search_form_for #search do |f| %>
<%= f.condition_fields do |c| %>
<div class="field">
<%= c.attribute_fields do |a| %>
<%= a.attribute_select %>
<% end %>
<%= c.predicate_select :only => [:cont, :not_cont, :matches]%>
<%= c.value_fields do |v| %>
<%= v.text_field :value %>
<% end %>
</div>
<% end %>
I expect to see Consignee Name in the dropdown list, but only see 'Name' at the bottom. When I select this and press search it returns with an error:
undefined method `type' for nil:NilClass
on line: Manifest.includes(:vessel, :pod, :pol, :por, :del, :consignee).ransack(params[:q])
manifest.rb (define ransackable_attributes only for this model)
class Manifest < ApplicationRecord
belongs_to :shipper
belongs_to :consignee
...
private
def self.ransackable_attributes(auth_object = nil)
super - ['id', 'created_at', 'updated_at', 'consignee_id']
end
end
consignee.rb (define ransackable_attributes for this model)
class Consignee < ApplicationRecord
has_many :manifest, dependent: :destroy
...
private
def self.ransackable_attributes(auth_object = nil)
if auth_object == :manifest_search
['name']
else
super
end
end
end
manifest_controller.rb
...
def index
#search = ransack_params
#search.build_grouping unless #search.groupings.any?
#manifests = #search.result(distinct: true)
#search.build_condition
...
private
def ransack_params
Manifest.ransack(params[:q], auth_object: :manifest_search)
end
index.html.erb (add the associations parameter to attribute_select)
<%= search_form_for #search do |f| %>
<%= f.condition_fields do |c| %>
<div class="field">
<%= c.attribute_fields do |a| %>
<%= a.attribute_select :associations => ["consignee"] %>
<% end %>
<%= c.predicate_select :only => [:cont, :not_cont, :matches]%>
<%= c.value_fields do |v| %>
<%= v.text_field :value %>
<% end %>
</div>
<% end %>
note that the auth_object conditional is optional, but it allows you to have different search pages with different attributes. For example, the consignee page could have its own search form showing all of consignee's attributes, while the manifest search shows only the consignee name as a searchable attribute.
I removed the includes to simplify the code; it's not required to make the search work. If you need those associations pre-loaded, you can put it back in.
In my application I have multiple user roles defined using an enum:
enum role: { staff: 0, clinician: 1, admin: 2 }
Staff users each belong to a university:
Staff Concern:
require 'active_support/concern'
module StaffUser
extend ActiveSupport::Concern
included do
belongs_to :university
has_many :patients
has_many :referral_requests
validates :university_id, presence: true, if: :staff?
end
University Model
class University < ApplicationRecord
has_many :staffs, -> { where role: :staff}, class_name: "User"
has_many :clinicians, through: :lists
has_many :whitelists
belongs_to :market
validates :market_id, presence: true
end
I have a dropdown select menu for Staff Doctor on a patients/new view where I want to display a list of staff users who belong to the same university as the current user, but I can't seem to get it to work. Currently, the dropdown only contains the prompt text. What am I doing wrong?
patients/new view:
<%= form_for(#patient) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="checkbox">
<h1>Tell us about your patient</h1>
<h2>Insurance</h2>
<% Insurance.all.each do |insurance| %>
<%= check_box_tag "patient[insurance_ids][]", insurance.id, #patient.insurance_ids.include?(insurance.id), id: dom_id(insurance) %>
<%= label_tag dom_id(insurance), insurance.name %><br>
<% end %>
<h2>Presenting Concerns</h2>
<% Concern.all.each do |concern| %>
<%= check_box_tag "patient[concern_ids][]", concern.id, #patient.concern_ids.include?(concern.id), id: dom_id(concern) %>
<%= label_tag dom_id(concern), concern.name %><br>
<% end %>
<h2>Staff Doctor</h2>
<%= select_tag "patient[staff_doctor_id]", options_from_collection_for_select(User.where("role = ? AND university_id = ?", "staff", #user.university_id), "id", "name"), prompt: "Select this patient's therapist" %>
</div>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %
Patients Controller:
class PatientsController < ApplicationController
before_action :require_login
def new
#user = current_user
#patient = current_user.patients.build
end
def index
authorize Patient
#patients = policy_scope(Patient)
end
def show
#patient = Patient.find(params[:id])
end
def edit
#patient = Patient.find(params[:id])
end
def update
#patients = Patient.all
#patient = Patient.find(params[:id])
if #patient.update_attributes(patient_params)
flash[:success] = "Patient Updated!"
render 'patients/index'
else
render "edit"
end
end
def create
#patient = current_user.patients.build(patient_params)
if #patient.save
flash[:success] = "Patient Created!"
redirect_to new_referral_request_path(patient_id: #patient.id)
else
Rails.logger.info(#patient.errors.inspect)
render 'patients/new'
end
end
private
def patient_params
params.require(:patient).permit(:age, :staff_doctor_id, :user_id, insurance_ids: [], gender_ids: [], concern_ids: [], race_ids: [])
end
end
Scopes in ActiveRecord are chainable:
User.staff.where(university: #user.university)
Chaining .where or scopes creates AND clauses. So all the conditions must apply.
Using ActiveRecord::Enum creates scopes for each of the enum states. So this is equivilent to:
User.where(role: :staff, university: #user.university)
When using an ActiveRecord::Enum you need to remember that the database stores integers - not strings:
User.where('role = 0') # staff
User.where('role = ?', User.statuses[:staff])
But there is no need to use a SQL string for this query.
A much better way to create selects and checkboxes is by using the rails collection helpers:
<%= form_for(#patient) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="checkbox">
<h1>Tell us about your patient</h1>
<h2>Insurance</h2>
<%= f.collection_check_boxes(:insurance_ids, Insurance.all, :id, :name) %>
<h2>Presenting Concerns</h2>
<%= f.collection_check_boxes(:concern_ids, Concern.all, :id, :name) %>
<h2>Staff Doctor</h2>
<%= f.collection_select(:staff_doctor_id, User.staff.where(university: #user.university), :id, :name, prompt: "Select this patient's therapist") %>
</div>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
Not only is this a lot less code, but binding the inputs to the form builder ensures that they "hold the value" when validations fail.
The example code below is a contrived example of an attempt at a form object where it is probably overkill to utilize a form object. Nonetheless: it shows the issue I am having:
I have two models: a User and an Email:
# app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
# app/models/user.rb
class Email < ApplicationRecord
belongs_to :user
end
I want to create a form object which creates a user record, and then creates three associated email records.
Here are my form object classes:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :email_forms
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
user = User.create(name: name)
email_forms.each do |email|
Email.create(user: user, email_text: email.email_text)
end
end
end
# app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
# DON'T THINK I WOULD PERSIST DATA HERE
# INSTEAD DO IT IN THE user_form
end
end
Notice: the validations on the form objects. A user_form is considered to be invalid if it's name attribute is blank, or if the email_text attribute is left blank for any of the email_form objects inside it's email_forms array.
For brevity: I will just be going through the new and create action of utilizing the user_form:
# app/controllers/user_controller.rb
class UsersController < ApplicationController
def new
#user_form = UserForm.new
#user_form.email_forms = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {email_forms: [:_destroy, :id, :email_text, :user_id]})
end
end
Lastly: the form itself:
# app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
# app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
# MESSY, but couldn't think of a better way to do this...
<% unique_index = 0 %>
<% user_form.email_forms.each do |email_form| %>
<div class="field">
<%= label_tag "user_form[email_forms][#{unique_index}][email_text]", "Email Text" %>
<%= text_field_tag "user_form[email_forms][#{unique_index}][email_text]" %>
</div>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The form does render:
And here is the form's html:
I go to submit the form. Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "email_forms"=>{"0"=>{"email_text"=>"test_email_1"}, "1"=>{"email_text"=>"test_email_2"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
What should happen is the form should be re-rendered and nothing persisted because the form_object is invalid: All three associated emails must NOT be blank. However: the form_object thinks it is valid, and it blows up in the persist! method on the UserForm. It highlights the Email.create(user: user, email_text: email.email_text) line and says:
undefined method `email_text' for ["0", {"email_text"=>"test_email_1"}]:Array
Clearly there are a couple things going on: The nested validations appear to not be working, and I am having trouble rebuilding each of the emails from the params hash.
Resources I have already examined:
This Article seemed promising but I was having trouble getting it to work.
I have attempted an implementation with the virtus gem and the reform-rails gem. I have pending questions posted for both of those implementations as well: virtus attempt here and then reform-rails attempt here.
I have attempted plugging in accepts_nested_attributes, but was having trouble figuring out how to utilize that with a form object, as well as a nested form object (like in this code example). Part of the issue was that has_many and accepts_nested_attributes_for do not appear to be included in ActiveModel::Model.
Any guidance on getting this form object to do what is expected would be very much appreciated! Thanks!
Complete Answer
Models:
#app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
#app/models/email.rb
class Email < ApplicationRecord
belongs_to :user
end
Controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Form Objects:
#app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :emails
validates :name, presence: true
validate :all_emails_valid
def emails_attributes=(attributes)
#emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
#emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email_form|
Email.new(email_text: email_form.email_text)
end
user.emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
end
Views:
app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I have a nested relationship where dashboard has many rewards, and I am trying to add a fields_for to the page in order to edit the rewards. Unfortunately, it doesn't seem to be working and I don't know why.
Here's what I have.
Dashboard model:
class Dashboard < ActiveRecord::Base
belongs_to :manager
has_many :rewards
accepts_nested_attributes_for :rewards, allow_destroy: true
end
Rewards model:
class Reward < ActiveRecord::Base
belongs_to :dashboard
end
Dashboard controller:
class DashboardsController < ApplicationController
before_action :authenticate_manager!
# Requires user to be signed in
def index
#dashboards = Dashboard.all
end
def new
#dashboard = Dashboard.new
end
def edit
#dashboard = Dashboard.find(params[:id])
end
def create
#dashboard = Dashboard.new(dashboard_params)
#dashboard.save
if #dashboard.save
redirect_to dashboard_path(#dashboard)
else
render :action => new
end
end
def update
#dashboard = Dashboard.find(params[:id])
if #dashboard.update(dashboard_params)
redirect_to :action => :show
else
render 'edit'
end
end
def show
#dashboard = Dashboard.find(params[:id])
end
def destroy
#dashboard = Dashboard.find_by_id(params[:id])
if #dashboard.destroy
redirect_to dashboards_path
end
end
private
def dashboard_params
args = params.require(:dashboard).permit(:title, :description, :rewards, {rewards_attributes: [ :id, :title, :referralAmount, :dashboardid, :selected, :_destroy] } )
args
end
end
Form in dashboards view:
<%= form_for :dashboard, url: dashboard_path(#dashboard), method: :patch do |f| %>
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :description %><br>
<%= f.text_field :description %>
</p>
<%= f.fields_for :rewards do |reward| %>
<%= reward.label :title %><br>
<%= reward.text_field :title %>
<%= reward.check_box :_destroy %>
<%= reward.label :_destroy, "Remove reward" %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
I went ahead and manually added rewards to the database through the rails console and it worked beautifully, but they are not showing up on the page. They will show up if I iterate through them like so
<% if #dashboard.rewards.any? %>
<ul>
<% #dashboard.rewards.each do |reward| %>
<li><%= reward.title %></li>
<li><%= reward.referralAmount %></li>
<% end %>
</ul>
<% else %>
<p>no rewards</p>
<% end %>
However the fields_for does not display the rewards or their content and resultingly allow one to edit them.
Let me know if you need further information/code.
Try to modify your:
View:
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_for #dashboard, url: dashboard_path(#dashboard) do |f| %>
........
<% end %>
Controller (has_many relationship):
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
private
def dashboard_params
params.require(:dashboard).permit(:title, :description,
rewards_attributes: [
:id,
:title,
:referralAmount,
:dashboardid,
:selected,
:_destroy
])
end
You don't have to set the method: patch if form.
Once you got in edit page, Rails will use the update action in controller when form submission.
To check it, run rake routes,
you will see somsthing like this:
PATCH /dashboards/:id(.:format) dashboards#update
PUT /dashboards/:id(.:format) dashboards#update
In controller you need to give build
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
"build" is just create a new object in memory so that the view can take this object and display something, especially for a form.
Hope it helps for you
You should build object before nested form. You can add whatever you want that object.
Try it in controller;
def new
#dashboard = Dashboard.new
3.times do
#dashboard.build_reward
end
end
Try setting an "#rewards" instance variable in your dashboards edit method (where #rewards = #dashboard.rewards). Then replace :rewards with #rewards.
Edit:
I believe my initial answer is inapproriate for your exact question (while it would be helpful on say the page to show a specific dashboard and its rewards). The answers above are on the right track re:
refining your params method per #aldrien.h;
Adding #santosh dadi's suggestion of
#dashboard.rewards.build
(assuming you only want one rewards fields on a form for "new")
Finally though, to avoid making fake information for a new rewards form, adding to the top of your Dashboards model:
accepts_nested_attributes_for :rewards, reject_if: lambda {|attributes| attributes['title'].blank?}
http://guides.rubyonrails.org/form_helpers.html#nested-forms
I'm working on a shows when a store was last visited. I want to be able to update multiple stores at once if they were all visited on the same day.
I think I have most of the code but I can't figure out how to get rid of the mass assignment error
Can't mass-assign protected attributes: date_visited(1i), date_visited(2i), date_visited(3i)
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"/yr8kLOyrTRGPfG1f/I5ilY/QB6GUx9IhQj6WiBaibM=",
"store_ids"=>["4",
"5"],
"visit"=>{"date_visited(1i)"=>"2012",
"date_visited(2i)"=>"11",
"date_visited(3i)"=>"14"},
"commit"=>"Save Visit"}
Model
class Visit < ActiveRecord::Base
attr_accessible :date_visited, :spent, :store_id
belongs_to :
end
Controller
def update_multiple
#visits = Store.find(params[:store_ids])
#visits.each do |visit|
visit.update_attributes(params[:visit])
end
flash[:notice] = "Updated products!"
redirect_to stores_path
end
View
<%= form_for :visit, :url => update_multiple_visits_path, :html => { :method => :put } do |f| %>
<ul>
<% #visits.each do |visit| %>
<%= hidden_field_tag "store_ids[]", visit.id %>
<% end %>
</ul>
<div class="field">
<%= f.label :date_visited %><br />
<%= f.date_select :date_visited %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<ol id="route">
<% #visits.each do |store| %>
<%= content_tag_for :li, store do %>
<%= "#{store.store} - #{store.address}" %>
<% end %>
<% end %>
</ol>
Most likely, you are missing the attr_accessible :your_model_attributes is this case, :visits_attributes on your activerecord model definition.
Also, your params should look like
{ visits =>
{ id_1 =>
{ :store_id
:attributes_for_visit_1 }
}
{ id_2 =>
{ :store_id
:attributes_for_visit_2 }
}
} # and so on....
# visits_controller.rb
def update_nultiple_visits
#visits = Visits.find(params[:visits].keys).each{|visit|visit.update_attributes!}
end
Add this to your Store model
attr_accessible :visits_attributes
accepts_nested_attributes_for :visits
And I'd suggest changing your controller to this:
def update_multiple
#stores = Store.find(params[:store_ids])
#stores.each do |store|
store.update_attributes(params[:visit])
end
flash[:notice] = "Updated products!"
redirect_to stores_path
end
Helper date_select generate three a select tags (for year, month and day).
You can concatenate its before updating attributes.
For example:
Date.civil(params[:visit][:date_visited(1i)].to_i, params[:visit][:date_visited(2i)].to_i, params[:visit][:date_visited(3i)].to_i)