I want to write a method which will work like this one
def create_new_car(salon)
Car.create name: salon.offer.name, description: salon.offer.description, photo: salon.offer.photo, dealer_id: salon.dealer_id
end
but i want to keep it DRY. is there a way to pass those attributes by, i dont know, iterating through array of attributes by passing a block?
You can pass a block to create:
def create_new_car(salon)
Car.create do |car|
car.name = salon.offer.name
car.description = salon.offer.description
car.photo = salon.offer.photo
car.dealer_id = salon.dealer_id
end
end
You could also set some attributes as the first parameter and then pass a block:
Car.create(name: salon.offer.name) do |car|
car.description = salon.offer.description
#...
end
You can implement any logic you want inside that block to assign the Car properties, like this:
attributes = ["name", "description", "photo", "dealer_id"]
Car.create do |car|
attributes.each { |a| car.send( "#{a}=", salon.offer.send(a) )
end
Please try this
array_of_attrbiutes = [{name: salon.offer.name...}, {name: }, {}...]
def create_new_car(array_of_attributes)
Car.create array_of_attributes
end
end
Please see https://github.com/rails/rails/blob/b15ce4a006756a0b6cacfb9593d88c9a7dfd8eb0/activerecord/lib/active_record/associations/collection_proxy.rb#L259
Related
I have a class (not active record) and I would like to create objects from API data.
Since fields name/structure don't match, I don't think that it's possible to use params as we would use with forms.
That's why I'm mapping the attributes as follow:
job = Job.new()
job.id = attributes['id']
job.title = attributes['fields']['title']
job.body = attributes['fields']['body-html']
job.how_to_apply = attributes['fields']['how_to_apply-html'].presence
attributes['fields']['city'].each { |city| job.cities << city['name'] } if attributes['fields']['city']
attributes['fields']['country'].each { |country| job.countries << country['name'] }
job.start_date = Date.parse(attributes['fields']['date']['created'])
job.end_date = Date.parse(attributes['fields']['date']['closing'])
attributes['fields']['source'].each { |source| job.sources << source['name'] }
attributes['fields']['categories'].each { |category| job.categories << category['name'] }
job
attributes is the data part of a JSON response.
What do you guys think?
A more readable way is to have an initializer in Job and call it like this:
job = Job.new(
id: attributes['id'],
title: attributes['fields']['title'],
body: attributes['fields']['body-html'],
how_to_apply: attributes['fields']['how_to_apply-html'].presence,
cities: attributes['fields']['city']&.map { |city| city['name'] },
countries: attributes['fields']['country'].map { |country| country['name'] },
start_date: Date.parse(attributes['fields']['date']['created']),
end_date: Date.parse(attributes['fields']['date']['closing']),
sources: attributes['fields']['source'].map { |source| source['name'] },
categories: attributes['fields']['categories'].map { |category| category['name'] }
)
initializer can take named parameters or just a options hash (not recommended):
class Job < ...
def initializer(id:, title:, cities: nil, and_so_on__:)
self.id = id
# ...
end
end
You can use .tap method, its a little bit cleaner this way. Also some things can be moved to methods, for example:
fields = attributes['fields']
job = Job.new.tap do |j|
j.id = attributes['id']
j.title = fields['title']
j.body = fields['body-html']
j.how_to_apply = fields['how_to_apply-html'].presence
j.start_date = date_parser(fields['date']['created'])
j.end_date = date_parser(fields['date']['closing'])
j.countries = fields['country'].map { |country| country['name'] }
j.cities = fields['city']&.map { |city| city['name'] }
(...)
end
def date_parser(date)
Date.parse(date)
end
Since this question is tagged Rails you can use ActiveModel::Model and ActiveModel::Attributes to create a rich model with typecasting, validations etc.
Then just create a factory method to create model instances from raw JSON:
class Job
include ActiveModel::Model
include ActiveModel::Attributes
attribute :id, :integer
attribute :title, :string
attribute :body, :string
attribute :how_to_apply, :string
attribute :start_date, :date
attribute :end_date, :date
# Unfortunately ActiveModel::Attributes does not support array attributes
attr_accessor :city
attr_accessor :country
attr_accessor :source
attr_accessor :categories
def self.from_json(**attributes)
# use attributes.fetch('fields') instead if you
# want to raise and halt execution
fields = attributes['fields']
new(attributes.slice('id', 'title')) do |job|
job.assign_attributes(
body: fields['body-html'],
how_to_apply: fields['how_to_apply-html'],
city: fields['city']&.map {|c| c['name'] },
country: fields['country']&.map {|c| c['name'] },
start_date: fields.dig('date', 'created'),
end_date: fields.dig('date', 'closing'),
source: fields['source']&.map {|s| s['name'] },
categories: fields['categories']&.map {|c| c['name'] }
) if fields
end
end
end
If this method glows to an unruly size or if the complexity increases you can use the adapter pattern or a serializer.
Since fields name/structure don't match, I don't think that it's possible to use params as we would use with forms.
This is not quite true. ActionController::Parameters is really just a Hash like object and you can use .merge to manipulate it just like a hash:
params = ActionController::Parameters.new(json_hash)
.permit(:id, :title, fields: {})
params .slice(:id, :title).merge(
how_to_apply: params[:fields]['how_to_apply-html'],
# ...
)
The search method is non-crud action and map is a private method, restaurant, dish, location, pictures are models. these models data contains an array. so how I write test case for map method and search method. restaurant and location has HABTM association, and also restaurant and dish has HABTM association, restaurant and pictures have a polymorphic association, and also dish and pictures has a polymorphic association
def search
map
if params[:name]
#items = Dish.search(params[:name])
end
if params[:price]
#items = Dish.sortby_price(params[:price]).search(params[:name])
end
if params[:ratings]
#items = Dish.sortby_ratings(params[:name])
end
if params[:rating]
#items = Dish.sortby_rating(params[:rating])
end
if params[:category]
#items= Dish.sortby_dietary(params[:category]).search(params[:name])
end
if params[:restaurant]
#restaurants =
Restaurant.find(params[:restaurant])
#items = #restaurants.dishes
end
end
private
def map
#items = Dish.search(params[:name])
restaurants = []
locations = []
pictures = []
#items.each do |d|
#restaurants = d.restaurants
restaurants.push(#restaurants)
d.restaurants.each do |r|
#pictures = r.pictures
pictures.push(#pictures)
#locations = r.locations
locations.push(#locations)
end
end
gon.restaurants = restaurants
gon.locations = locations
gon.pictures = pictures
x = []
#items.each do |d|
#restaurants = d.restaurants
d.restaurants.each do |r|
x.push(r.id)
end
end
y = []
x.each do |x|
r = Restaurant.find(x)
d = r.dishes.count
y.push(d)
end
gon.dishes_count = y
end
Some people say that there is no need to test private methods. But in a company i'm working for we do test private methods.
For your case I'd recommend to do this:
test method #map separately from action #search. You need to check that gon, #items, #restaurants, #pictures, #locations objects got populated correctly.
You can test private methods by using method #send.
Example:
describe '#map' do
subject { controller.send(:map) }
# you would need to stub params method
before { allow(controller).to receive(:params).and_return({ name: 'my name' }) }
it { expect(controller.instance_variable_get(:#items)).to include/not be_blank/... }
end
Test method #search without actually calling method map.
Example:
describe '#search' do
before { allow(controller).to receive(:map) }
# you can set different context where you test cases with different parameters
context 'when params[:name] and params[:ratings] exist' do
before { get :search, { name: '...', ratings: '...' } }
it {...}
end
end
I'm aware I can do something like this:
#object.update_attributes(date: params[:date]) if params[:date].present?
#object.update_attributes(date: params[:name]) if params[:name].present?
#object.update_attributes(date: params[:thing]) if params[:thing].present?
#object.update_attributes(date: params[:item]) if params[:item].present?
But is there a way to do all of this at once?
Something like:
#object.update_attributes(object_params)
where it won't put nulls in my database if they aren't passed in through the url. So I can call
Object.update(date: Date.today, name: "ryan") and it will only update these items.
You can try this
#object.update_attributes(params)
Also, it's a good practice to use strong params, putting this in the end of your controller
private
def object_params
params.require(:object).permit(:date, :name, :item, :thing)
end
end
and use it as
#object.update_attributes(object_params)
Just select params which are not nil in your controller and use #object.update_attributes(object_params) as usual
def object_params
params.require(:object).permit(:date, :name, :thing, :item).select { |k, v| !v.nil? }
end
Try this:
keys = [:date, :name, :item, :thing]
object_params = params.slice(*keys).delete_if { |k,v| v.nil? }
#object.update_attributes(object_params)
You could also do this if the param keys match the model columns:
object_params = params.slice(*Model.column_names).delete_if { |k,v| v.nil? }
#object.update_attributes(object_params)
I have a method in a helper, here is the code:
def top_menu_output(parent = Category.where(parent_id: nil))
parent.each do |p|
content_tag(:li) do
parent_id = p.id
if Category.where(parent_id: parent_id).exists?
link_to(p.title, p.page_name, class: "parent")
content_tag(:ul, class: "unstyled") do
subparent = Category.where(parent_id: parent_id)
content_tag(:li) do
top_menu_output(subparent)
end
end
elsif
link_to(p.title, p.page_name)
end
end
end
end
and I call the method in a view like this
<%top_menu_output%>
but it renders nothing. What do I do to render all the links and li's? Thank you for your answers.
P.S. If I put the code right in the view it works just fine, but the view is obviously not the right place for the method.
P.P.S. If I call the method like this <%=top_menu_output%> it renders all the stuff from my db CATEGORY ID: 1, PARENT_ID: NIL, PAGE_NAME: "", KEYWORDS: "", DESCRIPTION: "", SEO_TEXT_LEFT: "", SEO_TEXT_RIGHT: "", CREATED_AT: "2014-04-19 22:08:55", UPDATED_AT: "2014-04-19 22:08:55"...
The return value of each is the collection that is iterated over, so you are generating a whole bunch of html but then returning something else
You could change that to
parent.collect do |p|
content_tag(:li) do
...
end
end.join
This would collect the html generated by each iteration through the block, concatenate them all and return that. As pointed out in the comments, you also need to use <%= or you'll never see any output
Thanks Frederick, changing each to collect is almost what I want. But it is also necessary to use concat and + so the properly working method is:
def top_menu_output(parent = Category.where(parent_id: nil))
parent.collect do |p|
content_tag(:li) do
parent_id = p.id
if Category.where(parent_id: parent_id).exists?
link_to(p.title, p.page_name, class: "parent") +
content_tag(:ul, class: "unstyled js-menu") do
subparent = Category.where(parent_id: parent_id)
top_menu_output(subparent)
end
elsif
concat link_to(p.title, p.page_name)
end
end
end.join.html_safe
end
I have a dilema. There's a huge function in my controller to standardise loads of different types of data into one list for the view. So I have this kind of way of handling it at the moment:
customer.notes.each do |note|
to_push = {
id: note.id,
title: 'Contact Note',
type: 'note',
description: note.notes,
user: note.user,
date: note.date,
action: nil,
extras: note.customer_interests,
closed: false,
colour: '#9b59b6'
}
history.push to_push
end
I want to move that out of the controller into the model but I'm not too sure how. I ideally want a method like customer.notes.format_for_timeline but I can't figure out how to iterate over results like that in a self method within the class.
Thanks
I found out how. Using a self method then all:
def self.format
all.each do |item|
# Manipulate items here
end
end
However, I ended up having a method like this:
def format
{
id: id,
note: 'Contact Note',
# Etc
}
end
Then just used:
customer.notes.map {|i| i.format }