Rails create two associatiated models in one New-Create action - ruby-on-rails

I have such models:
class Doc
has_many :photos
end
and
class Photo
belongs_to :doc
end
all photos uploaded to the cloud with CarrierWave-Paperclip like approach.
DocController#new prebuilds Doc with:
#doc = Doc.new
And only after saving this new Doc, in update action i can really upload photos to existed Doc object with:
#doc.photos << some_new_photo
But i want this feature in doc#new action. So, how i can upload photos like prebuilded Photo objects and add them to prebuilded Doc with #doc.photos << [photos] at the same time?
UPD:
Main problem, that when i make doc#new - i don't really know how many photos i'll upload during using form. So i have dynamically builded array of photos, that shouldn't be saved to DB, if associated Doc not saved/

You can use a somewhat convoluted Rails feature called accepts_nested_attributes which lets you create any number of associated objects in one go.
Basically your create call would end up accepting something like this:
{ :doc => { :name => 'somname', :date => Time.now, :photos_attributes => [
{ :filename => 'funnybear.gif', :filesize => '120kb' },
{ :filename => 'happybear.gif', :filesize => '72kb' },
{ :filename => 'angrybear.gif', :filesize => '240kb' }
]}}

You can use blocks
#doc = Doc.new do |doc|
doc.photos << some_new_photo
end
or you can redefine initialize method
class Doc
def initialize
#photos << some_new_photos
end
end

Or you can use build:
#doc = Doc.new
params[:photos].each do |some_new_photo|
#doc.photos.build some_new_photo
end
if #doc.save!
... etc

Related

Rails Dry up Find or Create

I have the following method in my model which uses find_or_create_by to find or create a new product.
def self.save_prod(product)
Product.find_or_create_by_prod_id(product)
product_data = ItemData.get_product_data(product)
p.update_attributes(
:prod_id => product,
:upc => product_data[:upc],
:title => product_data[:title]
)
end
The ItemData.get_product_data() method is a module method which calls an API to fetch product data:
def self.get_product_data(product)
url_raw = URI.parse("http://www.api.com/v1/itemid=#{product}")
url = Net::HTTP.get_response(url_raw).body
#resp = JSON.parse(url)
#title = Sanitize.clean(#resp["serviceResult"]["itemName"]).strip
#upc = #resp["serviceResult"]["uPC"]
{:title => #title, :upc => #upc}
end
This works as expected, however I know it can be a LOT more efficient, by not calling the ItemData.get_product_data() method every time the save_prod() method is called. How can I add new product data without having to call the ItemData.get_product_data() if a product already exists.
Another way to doing it. This would return the Product object if it is already present otherwise it will create it from api and return the new object.
def self.save_prod(product)
Product.find_by_prod_id(product) || Product.create( ItemData.get_product_data(product) )
end
Modify the api call to return a hash with prod_id. Not sure why you are converting title and upc to class variables here. It could lead to problems if they are used extensively.
def self.get_product_data(product)
url_raw = URI.parse("http://www.api.com/v1/itemid=#{product}")
url = Net::HTTP.get_response(url_raw).body
#resp = JSON.parse(url)
#title = Sanitize.clean(#resp["serviceResult"]["itemName"]).strip
#upc = #resp["serviceResult"]["uPC"]
{:title => #title, :upc => #upc, :prod_id => product}
end
Instead of doing a find or create use find or initialize by . Change your code to following :
prod = find_or_initialize_by_prod_id(product)
if prod.new_record?
prod.save!
product_data = ItemData.get_product_data(product)
prod.update_attributes(
:prod_id => product,
:upc => product_data[:upc],
:title => product_data[:title]
)
end
by using find_or_initalize you can distinguish whether the record was created or found by using new_record method. If new you can save and make an API call and do whatever you want.

Build has_many :through for new Object

I might be doing this wrong overall, but maybe someone will be able to chirp in and help out.
Problem:
I want to be able to build a relationship on an unsaved Object, such that this would work:
v = Video.new
v.title = "New Video"
v.actors.build(:name => "Jonny Depp")
v.save!
To add to this, these will be generated through a custom method, which I'm attempting to modify to work, that does the following:
v = Video.new
v.title = "Interesting cast..."
v.actors_list = "Jonny Depp, Clint Eastwood, Rick Moranis"
v.save
This method looks like this in video.rb
def actors_list=value
#Clear for existing videos
self.actors.clear
value.split(',').each do |actorname|
if existing = Actor.find_by_name(actorname.strip)
self.actors << existing
else
self.actors.build(:name => actorname.strip)
end
end
end
What I expect
v.actors.map(&:name)
=> ["Jonny Depp", "Clint Eastwood", "Rick Moranis"]
Unfortunatey, these tactics neither create an Actor nor the association. Oh yeah, you might ask me for that:
in video.rb
has_many :actor_on_videos
has_many :actors, :through => :actor_on_videos
accepts_nested_attributes_for :actors
I've also tried modifying the actors_list= method as such:
def actors_list=value
#Clear for existing videos
self.actors.clear
value.split(',').each do |actorname|
if existing = Actor.find_by_name(actorname.strip)
self.actors << existing
else
self.actors << Actor.create!(:name => actorname.strip)
end
end
end
And it creates the Actor, but I'd rather not create the Actor if the Video fails on saving.
So am I approaching this wrong? Or have I missed something obvious?
Try this:
class Video < ActiveRecord::Base
has_many :actor_on_videos
has_many :actors, :through => :actor_on_videos
attr_accessor :actors_list
after_save :save_actors
def actors_list=names
(names.presence || "").split(",").uniq.map(&:strip).tap do |new_list|
#actors_list = new_list if actors_list_changes?(new_list)
end
end
def actors_list_changes?(new_list)
new_record? or
(actors.count(:conditions => {:name => new_list}) != new_list.size)
end
# save the actors in after save.
def save_actors
return true if actors_list.blank?
# create the required names
self.actors = actors_list.map {|name| Actor.find_or_create_by_name(name)}
true
end
end
You cannot assign a child to an object with no id. You will need to store the actors in your new video object and save those records after the video has been saved and has an id.

Can't get Twitter gem search results in the model

Still very new to Rails. How do I call my method in calltwitter.rb file located in my lib folder from my model? Basically I want the array returned from the calltwitter.rb into my model so I can store it.
I have these two classes:
lib/twitter/calltwitter.rb
require 'rubygems'
require 'twitter'
class CallTwitter
def search(search_string)
Twitter.search(search_string, :rpp => 5, :lang => "en", :result_type => "mixed").map do |result|
search_tweets << {:image_url => result.profile_image_url, :from_user => result.from_user, :tweet => result.text, :tweeteddate => result.created_at}
end
return search_tweets
end
and
require './lib/twitter/CallTwitter.rb'
class Tweet < ActiveRecord::Base
def get_search_tweets
search_tweets = CallTwitter.new
search_tweets.search("search string")
end
end
I am not sure but could you please try
**edit this in config/application.rb Hope it will help.
config.autoload_paths += %W(#{config.root}/lib/twitter/CallTwitter.rb)
Had to use "self" like so:
def self.get_search_tweets
search_tweets = CallTwitter.new
search_tweets.search("search string")
end
Also had to reload the rails console.
rails c

Rails: upload cvs file to process it as a hash

I have this model:
class Survey < ActiveRecord::Base
attr_accessor :csvFile_file_name
has_attached_file :csvFile, :path => ":rails_root/public/:class/:attachment/:id/:style_:basename.:extension"
serialize :content, Hash
#after_save :do_cvs_process
def do_csv_process
product = {}
FasterCSV.foreach(self.csvFile.path, :headers => true, :col_sep => ",") do |row|
row.to_hash.each do |key, value|
product[key.underscore.to_sym] = value
end
end
self.update_column(:content, {:first => product})
end
end
I have several problems:
Because of standard browser security, I have to upload file and save it before processing it with csv to assign it as a hash to my :content attribute... That's why I'm using update_column to avoid callbacks. Is there a clever way to do it?
It does not work! When back to the view <%= #survey.content %> rails tells me that it found an array when it expected a hash.
def self.import_csv_file(iFileName)
c = CSV.open iFileName
header= c.first.map{ |i| i.to_s.strip.downcase; }
c.each { |row| import_hash( Hash[*header.zip(row).flatten] ); }
c.close
end
My product class has an import_hash method that looks for lowercase/spacefree headers and matches them to fields in the product.
product.name = hash['productname'] || hash['name'] #for example.
use faster_csv gem. Here are some quick links:
[SOURCE] https://github.com/JEG2/faster_csv
[DOCS] http://fastercsv.rubyforge.org/
[CHEATSHEET]http://cheat.errtheblog.com/s/faster_csv/
Please do some R&D on GitHub before pasting a questions, these questions are already there.

save! method for referenced attributes in mongoid

I use Rails 3.0.6 with mongoID 2.0.2. Recently I encountered an issue with save! method when overriding setter (I am trying to create my own nested attributes).
So here is the model:
class FeedItem
include Mongoid::Document
has_many :audio_refs
def audio_refs=(attributes_array, binding)
attributes_array.each do |attributes|
if attributes[:audio_track][:id]
self.audio_refs.build(:audio_track => AudioTrack.find(attributes[:audio_track][:id]))
elsif attributes[:audio_track][:file]
self.audio_refs.build(:audio_track => AudioTrack.new(:user_id => attributes[:audio_track][:user_id], :file => attributes[:audio_track][:file]))
end
end
if !binding
self.save!
end
end
AudioRef model (which is just buffer between audio_tracks and feed_items) is:
class AudioRef
include Mongoid::Document
belongs_to :feed_item
belongs_to :audio_track
end
And AudioTrack:
class AudioTrack
include Mongoid::Document
has_many :audio_refs
mount_uploader :file, AudioUploader
end
So here is the spec for the FeedItem model which doesn`t work:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_refs => [
{:audio_track => {:id => #audio_track.id}},
{:audio_track => {:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
As you can see, the reason I am overriding audio_refs= method is that FeedItem can be created from existing AudioTracks (when there is params[:audio_track][:id]) or from uploaded file (params[:audio_track][:file]).
The problem is that #feed_item.audio_refs.length == 0 when I run this spec, i.e. audio_refs are not saved. Could you please help me with that?
Some investigation:
1) binding param is "true" by default (this means we are in building mode)
I found a solution to my problem but I didnt understand why save method doesnt work and didn`t make my code work. So first of all let me describe my investigations about the problem. After audio_refs= is called an array of audio_refs is created BUT in any audio_ref is no feed_item_id. Probably it is because the feed_item is not saved by the moment.
So the solution is quite simple - Virtual Attributes. To understand them watch corresponding railscasts
So my solution is to create audio_refs by means of callback "after_save"
I slightly changed my models:
In FeedItem.rb I added
attr_writer :audio_tracks #feed_item operates with audio_tracks array
after_save :assign_audio #method to be called on callback
def assign_audio
if #audio_tracks
#audio_tracks.each do |attributes|
if attributes[:id]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.find(attributes[:id]))
elsif attributes[:file]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.new(:user_id => attributes[:user_id], :file => attributes[:file]))
end
end
end
end
And the spec is now:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_tracks => [
{:id => #audio_track.id},
{:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
And it works fine!!! Good luck with your coding)
Check that audio_refs=() is actually being called, by adding debug output of some kind. My feeling is that your FeedItem.new() call doesn't use the audio_refs=() setter.
Here's the source code of the ActiveRecord::Base#initialize method, taken from APIdock:
# File activerecord/lib/active_record/base.rb, line 1396
def initialize(attributes = nil)
#attributes = attributes_from_column_definition
#attributes_cache = {}
#new_record = true
#readonly = false
#destroyed = false
#marked_for_destruction = false
#previously_changed = {}
#changed_attributes = {}
ensure_proper_type
populate_with_current_scope_attributes
self.attributes = attributes unless attributes.nil?
result = yield self if block_given?
_run_initialize_callbacks
result
end
I don't currently have an environment to test this, but it looks like it's setting the attributes hash directly without going through each attribute's setter. If that's the case, you'll need to call your setter manually.
Actually, I think the fact you're not getting an exception for the number of arguments (binding not set) proves that your setter isn't being called.

Resources