How to pull information from associated model (Rails) - ruby-on-rails

I'm running a search using the Ransack gem.
Controller code (for ItemsController)
#q = Item.search(params[:q])
#search_results = #q.result
After running the search, #search_results contains multiple Items (Item 1, Item 2, Item 3, …). Where each Item is a hash:
Item = { "id" => "12", "name" => "shovel", "user_id" => "2", "type" => "A" }
In this case, for each Item hash, I want to add an additional key-value pair that translates the user_id into the name that is associated with that instance of the User model (i.e., User has_many :Items and Item belongs_to :Users)
I tried in the same controller code section
#search_results.each do |item|
item[:user_name] = User.find_by_id(item.user_id).name
end
But I get an error that I can't write to item. I suspect Ransack has something to do with it though, because if I just print #search_results, instead of getting the actual Item data, I get #<ActiveRecord::Relation::ActiveRecord_Relation_Item:0x000001025e6c78>
Help greatly appreciated!
Btw, in case there's even an easier way, the reason I want to add another key/value pair with the user_name is because the ultimate output is a JSON that is being picked up by another part of the codebase, and we don't want to run two separate database queries.

Firstly, you're receiving a ruby object back, which I take means you can create a set of attributes for in the model. Why don't you try using a getter in your item model to set for you:
#app/models/item.rb
Class Item < ActiveRecord::Base
belongs_to :user #-> needs to be singular
attr_accessor :user_name
def user_name
self.user.name
end
end
This means that instead of having to append a user_name attribute from your controller, every Item object will automatically have the user_name attribute anyway
A refactor of this would also be to use the delegate method inside your Item model:
Class Item < ActiveRecord::Base
belongs_to :user
delegate :name, to: :user, prefix: true #-> allows you to call #item.user_name
end

Related

Convert an Object to hash then save it to user's column

Could not find nothing close to what I'm trying to do. I want to store an object into a user's column. That column is in the form of an array:
#postgres
def change
add_column :users, :interest, :string, array: true, default: '{}'
end
I have another model called FooBar setup for other use. Each user has unique information inside as I've added a user_id key.
Im trying to make more sense:
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest.push(FooBar.find(#user)) # This just stores the object name itself ;)
end
So when I call u1 = FooBar.find(1) I get value return in hash. I want when I say u1.interest I get the same. The reason is, I need to target those keys on the user ie: u1.interest[0].support_id
Is this possible? I've looked over my basic ruby docs and nothing works. Oh..if I passed FooBar.find(#user).inspect I get the hash but not the way I want it.
Im trying to do something similar to stripe. Look at their data key. That's a hash.
Edit for Rich' answer:
I have, literally, a model called UserInterestSent model and table:
class UserInterestSent < ActiveRecord::Base
belongs_to :user
belongs_to :support # you can call this post
end
class CreateUserInterestSents < ActiveRecord::Migration
def change
create_table :user_interest_sents do |t|
t.integer :user_id # user's unique id to associate with post (support)
t.integer :interest_sent, :default => 0 # this will manually set to 1
t.integer :support_id, :default => 0 # id of the post they're on
t.timestamps # I need the time it was sent/requested for each user
end
end
end
I call interest interest_already_sent:
supports_controller.rb:
def interest_already_sent
support = Support.find(params[:id])
u = UserInterestSent.new(
{
'interest_sent' => 1, # they can only send one per support (post)
'user_id' => current_user.id, # here I add the current user
'support_id' => support.id, # and the post id they're on
})
current_user.interest << u # somewhere this inserts twice with different timestamps
end
And the interest not interests, column:
class AddInterestToUsers < ActiveRecord::Migration
def change
add_column :users, :interest, :text
end
end
HStore
I remembered there's a PGSQL datatype called hStore:
This module implements the hstore data type for storing sets of
key/value pairs within a single PostgreSQL value. This can be useful
in various scenarios, such as rows with many attributes that are
rarely examined, or semi-structured data. Keys and values are simply
text strings.
Heroku supports it and I've seen it used on another live application I was observing.
It won't store your object in the same way as Stripe's data attribute (for that, you'll just need to use text and save the object itself), but you can store a series of key:value pairs (JSON).
I've never used it before, but I'd imagine you can send a JSON object to the column, and it will allow you to to use the attributes you need. There's a good tutorial here, and Rails documentation here:
# app/models/profile.rb
class Profile < ActiveRecord::Base
end
Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
profile = Profile.first
profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
--
This means you should be able to just pass JSON objects to your hstore column:
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
def update
#profile = current_user.profile
#profile.update profile_params
end
private
def profile_params
params.require(:profile).permit(:x, :y, :z) #-> z = {"color": "blue", "weight": "heavy"}
end
end
As per your comments, it seems to me that you're trying to store "interest" in a User from another model.
My first interpretation was that you wanted to store a hash of information in your #user.interests column. Maybe you'd have {name: "interest", type: "sport"} or something.
From your comments, it seems like you're wanting to store associated objects/data in this column. If this is the case, the way you're doing it should be to use an ActiveRecord association.
If you don't know what this is, it's essentially a way to connect two or more models together through foreign keys in your DB. The way you set it up will determine what you can store & how...
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :interests,
class_name: "Support",
join_table: :users_supports,
foreign_key: :user_id,
association_foreign_key: :support_id
end
#app/models/support.rb
class Support < ActiveRecord::Base
has_and_belongs_to_many :users,
class_name: "Support",
join_table: :users_supports,
foreign_key: :support_id,
association_foreign_key: :user_id
end
#join table = users_supports (user_id, support_id)
by using this, you can populate the .interests or .users methods respectively:
#config/routes.rb
resources :supports do
post :interest #-> url.com/supports/:support_id/interest
end
#app/controllers/supports_controller.rb
class SupportsController < ApplicationController
def interest
#support = Support.find params[:support_id] # I need the post's id they are on
current_user.interests << #support
end
end
This will allow you to call #user.interests and bring back a collection of Support objects.
Okay, look.
What I suggested was an alternative to using interest column.
You seem to want to store a series of hashes for an associated model. This is exactly what many-to-many relationships are for.
The reason your data is being populated twice is because you're invoking it twice (u= is creating a record directly on the join model, and then you're inserting more data with <<).
I must add that in both instances, the correct behaviour is occurring; the join model is being populated, allowing you to call the associated objects.
What you're going for is something like this:
def interest_already_sent
support = Support.find params[:id]
current_user.interests << support
end
When using the method I recommended, get rid of the interest column.
You can call .interests through your join table.
When using the code above, it's telling Rails to insert the support object (IE support_id into the current_user (IE user_id) interests association (populated with the UserInterestSelf table).
This will basically then add a new record to this table with the user_id of current_user and the support_id of support.
EDIT
To store Hash into column, I suggest you to use "text" instead
def change
add_column :users, :interest, :text
end
and then set "serialize" to attribute
class User < ActiveRecord::Base
serialize :interest
end
once it's done, you can save hash object properly
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest = u.attributes # store hash
#user.save
end
To convert AR object to hash use object.attributes.
To store a custom hash in a model field you can use serialize or ActiveRecord::Store
You can also use to_json method as object.to_json
User.find(current_user.id ).to_json # gives a json string

Rails 4 field type for multiselect with predefined values

Heres the situation:
I have a Category model, which needs to have an attribute where the user can select multiple items from 3-4 predefined values (meaning only I can add more, the admins can't, so there is no separate model for those 3-4 options).
Enum would be great but with that, only 1 option can be selected.
Since I am using Postgres, I am thinking about using an array type attribute to store the selected values.
Is there a simpler, more efficient way to do this or another field type which I am just not aware of?
UPDATE (What I chose to do):
Migration (Postgres 9.3):
add_column :categories, :settings, :string, array: true, default: '{}'
Controller:
Added :settings => [] to permitted params.
View: <%= f.select :settings, %w[a b c], {}, :multiple => true %>
So if I would like to get all categories where setting 'a' is present then I an do:
Category.where("'a' = ANY (settings)")
I am thinking about using an array type attribute to store the selected values.
You can serialize your field to save values as array or hash in database. For this first you'll have to add a field in categories table by creating a migration
class some_migration
def change
add_column :categories, :some_field, :text
end
end
In model tell rails to use it as a serializable field
class Category < ActiveRecord::Base
serialize :some_field, Array
end
#this will allow you to do something like this:
category = Category.create(some_field: [some_value_1,some_value_2])
Category.find(category.id).preferences # => [some_value_1, some_value_2]

rails3-jquery-autocomplete passing parameter to scope query

I'm struggling to find an answer to a problem I have with the rails3-jquery-autocomplete gem.
I have 2 models. Environment & Property.
class Environment < ActiveRecord::Base
has_many :properties
name: string, envfile: string
class Property < ActiveRecord::Base
scope :with_environment_id, where(:environment_id => 14)
belongs_to :environment
name: string, value: string, environment_id: integer
I have a view which displays all the properties belonging to a particular environment and in that view the following code which auto searches based on the property name
<%= form_tag('tbd')%>
<%= autocomplete_field_tag :property_name, '', autocomplete_property_name_properties_path>
The properties controller
class PropertiesController < ApplicationController
autocomplete :property, :name, :scopes => [:with_environment_id]
and routes.rb has
get :autocomplete_property_name, :on => :collection
The autocomplete works fine but it returns returns all records in properties table. Where I would like only the properties belonging to the environment displayed in the view.
As you can see I've defined a scope in the property model in which I've hardcoded a valid environment_id to make this work as I would want it.
So my question is how do I pass the environment_id from the view/controller back into the scope?
Perhaps I'm tackling this from the wrong angle.
Apologies if it's a dumb question but it has stumped me all day.
def get_autocomplete_items(parameters)
super(parameters).where(:user_id => current_user.id)
end

Rails: create attributes in a specific order

In my Rails app I have Users. Users are asked for their home city and district / neighborhood.
class User < ActiveRecord::Base
belongs_to :city
belongs_to :district
end
class City < ActiveRecord::Base
has_many :users
has_many :districts
end
class District < ActiveRecord::Base
has_many :users
belongs_to :city
end
In forms I build the associations using a virtual attribute on the User model that accepts a string (more info below in case it's relevant).
In the console this all works great, but in the UI it's not working. The problem seems to be, I can get city_name through a form no problem, but when I try to assign city and district in the same form it always fails. In other words, mass assignment doesn't work.
#user.update_attributes(params[:user])
Instead the only thing I have been able to figure out is to manually set each key from a form submission, like:
#user.name = params[:user][:name] if params[:user][:name]
#user.city_name = params[:user][:city_name] if params[:user][:city_name]
#user.district_name = params[:user][:district_name] if params[:user][:district_name]
This approach works, but it's a pain, kind of brittle, and feels all wrong because it starts gunking the controller up with a lot of logic.
My question is:
Is there a way to create or update attributes in a specific order, ideally in the model so that the controller doesn't have to worry about all this?
Am I doing this wrong? If so, what approach would be better.
Extra Info
Here's how I build the associations using virtual attributes on the user model, in case that's relevant to any potential answerers:
I want users to be able to select a city by just typing in a name, like "Chicago, IL". This works fine, using a virtual attribute on the user model like so:
def city_name
city.try :full_name
end
def city_name=(string)
self.city = City.find_or_create_by_location_string( string )
end
It only makes sense for a user to find or create a district from the city they've chosen. This works slightly differently:
def district_name
district.try :name
end
def district_name=(string)
if self.city.nil?
raise "Cannot assign a district without first assigning a city."
else
self.district = self.city.districts.find_or_create_by_name( string )
end
end
In the model layer these things work fine, as long as both a city_name and district_name are set the district association works as expected.
I think you could do a few things to clean this up. First, you can use delegates to clean up the code. e.g.
delegate :name, :to => :city, :prefix => true, :allow_nil => true
that way you can do something like
user = User.create
user.city_name # nil
city = City.create(:name => 'chicago')
user.city = city
user.save
user.city_name # chicago
and it will just work.
Next, I would say take the name-to-id logic out of your model. You can do it either in the form (e.g. an ajax search puts the district id/city id into a hidden field), or in the controller. Then just assign the city/district as normal.

associate nested attributes with the ids they get after save

class Member < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts
end
params = { :member => {
:name => 'joe', :posts_attributes => [
{ :title => 'Kari, the awesome Ruby documentation browser!' },
{ :title => 'The egalitarian assumption of the modern citizen' },
]
}}
member = Member.create(params['member'])
After doing this I want is to map the elements in posts_attributes in params hash to the id's (The primary keys) after they are saved. Is there any thing I can do when accepts_nested_attributes_for builds or creates each record
PS: posts_attributes array may not contain index in sequence I mean this array might not contain index like 0,1,2 it can contain index like 0,127653,7863487 as I am dynamically creating form elements through javascript
also, I want is to associate only new records created in Post and not already existing Post
Thanks in Advance
Have you considered refreshing the posts association and grabbing the posts_attributes array in full?
Unfortunately, there is not a reliable way to do what you want. You could try looping over both and finding the IDs associated with the content using string matching, but without a field on the posts that is guaranteed to be a unique value, there's not an effective way to do it.
Although I'm not quite sure about what elements you want to assign with what ids, I think this approach would give you a hint.
You may assign a method name symbol to :reject_if, then put your logic into that method, like this:
class Member < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts, :reject_if => :reject_posts?
def reject_posts?(attrs)
# You can do some assignment here
return true if attrs["title"].blank?
post_exist = self.posts.detect do |p|
p.title == attrs["title"]
end
return post_exist
end
end

Resources