Pulling attribute names from associated child model to parent model - ruby-on-rails

class User
has_many :addresses
def csv_header
self.addresses.attribute_names
end
def csv_values
self.addresses.all do |addr|
addr.attributes.values
end
end
end
class Address
belongs_to :user
end
*i am trying to pull the attribute names of the address model to user model,but this method isn't working so can anyone help *

Not much needed here - I think you just need to map the addresses in csv_values.
class User
has_many :addresses
def csv_header
addresses.attribute_names
end
def csv_values
addresses.map do |addr|
addr.attributes.values
end
end
end
class Address
belongs_to :user
end
Does that fix this for you?
I'd be tempted to shift things around a little for clarity in the code and make use of delegate:
class user
...
delegate :attribute_names, to: :addresses, prefix: true, allow_nil: true
...
end
class Address
...
def self.mapped_values
all.map { |addr| addr.attributes.values }
end
...
end
Then you can just call user.addresses_attribute_names and user.addresses.mapped_values.
You can also just call Address.column_names to get the header array, if it will always stay consistent, as is likely to be the case.
Hope that helps!
Update based on comment:
To achieve the same for users, you can call the following:
Either call User.column_names or user.attribute_names to get the headers (on the class for the former, and an instance for the latter).
If you also need the users' mapped values, you can copy across the self.mapped_values method from the address model and use that. It's a little duplication, but for a pair of methods like this I wouldn't be inclined to separate these into a separate module.
Final tip - if you're calling the address methods from a collection of users (i.e. User.all) make sure you adjust it to include the addresses to avoid hitting your database in an inefficient way (User.includes(:addresses)...).

Related

Rails model with array field, modify push method

I've been trying to find an answer to my question but so far no luck.
I have a model with an array field and I'd like method calls to happen when something gets pushed into the array.
class Shop::Order
include Mongoid::Document
include Mongoid::Timestamps
embeds_many :items,class_name: 'Shop::OrderItem', inverse_of: :order
accepts_nested_attributes_for :items
field :price, type: Money, default: Money.new(0)
field :untaxed_price, type: Money, default: Money.new(0)
end
So when doing order.items << Shop::OrderItem.new(...)
I'd like a method foo to be called.
EDIT: Add reason
So the reason for this is that I want to update the price and untaxed_price of an order each time an item is added to it.
Does it need to happen as soon as you push it? Or can it happen before you save the order? If you can wait until you save, you can do this:
before_validate :update_tax_info
def update_tax_info
if items_changed?
calculate_tax #whatever that may be
end
end
Throwing it in a validation would allow that callback to be called without saving. You could call #order.valid? to update the tax information.
I think monkey patching << is a bad idea. I have two ideas:
Use some kind of observer which listens on create of OrderItem and performs appropriate action
Overwrite OrderItem.create method (or even better provide abstraction):
```
class OrderItem
def add(params)
if create(params)
calculate_something
end
end
end
```
This documentation gives you the range of choices that you have to implement the desired behavior: http://mongoid.org/en/mongoid/docs/callbacks.html
To paraphrase, you have the option of using callbacks like before_save and before_update to do your calculations, or you can implement an Observer class to do this for you.
You can also use the changed method to see if the items array has changed and whether you need to update the derived fields.
Here's some example code:
class OrderObserver < Mongoid::Observer
def before_save(order)
do_something
end
end
Do remember to instantiate your observer in application.rb using:
config.mongoid.observers = :order_observer

belongs_to association without an extra table

I have a model Download, with a table downloads. downloads has a field called ip_address, which stores an ip address as an integer. I want to set up an IpAddress model, but without a ip_addresses table, so I can do stuff like
Download.find(1).ip_address.to_s # '127.0.0.1'
Download.find(1).ip_address.to_i # 2130706433
Download.find(1).ip_address.downloads # SELECT * FROM downloads WHERE ip_address='2130706433'
IpAddress.find(2130706433).downloads # SELECT * FROM downloads WHERE ip_address='2130706433'
I want it to behave like:
class Download < ActiveRecord::Base
belongs_to :ip_address, :foreign_key => :ip_address
end
class IpAddress < ActiveRecord::Base
set_primary_key :ip_address
has_many :downloads, :foreign_key => :ip_address
end
but without having a useless table of ip addresses.
Is this possible?
EDIT
I found that ruby already has a IPAddr class.
So I did this:
require 'ipaddr'
class Download < ActiveRecord::Base
attr_accessible :ip, ...
def ip
#ip ||= IPAddr.new(read_attribute(:ip), Socket::AF_INET)
end
def ip=(addr)
#ip = IPAddr.new(addr, Socket::AF_INET)
write_attribute(:ip, #ip.to_i)
end
def self.for_ip(addr)
where(:ip => IPAddr.new(addr, Socket::AF_INET).to_i)
end
end
Then I can do lots of cool stuff
Download.new(:ip => '127.0.0.1').save
Download.for_ip('127.0.0.1').first.ip.to_i # 2130706433
belongs_to is really meant to specify an association between objects in two tables. But you're right, unless you need to store other associated data, storing IP addresses in a table is fairly useless.
However, you can use scopes to accomplish what you will want. You could have something like this in your Download model:
class Download < ActiveRecord::Base
scope :for_ip, lambda { |x| where(:ip_address => x) }
end
Then you would call
Download.for_ip(2130706433)
To get a list of downloads for that IP.
You could also add a class method instead:
class Download < ActiveRecord::Base
def self.for_ip(x)
where(:ip_address => x)
end
end
That might be handy if you want to convert from string to numeric IP addresses.
And, if you want an IPAddress class, you can add a method like this:
class IPAddress
def initialize(ip)
#presumably do some stuff here
#ip = ip
end
def downloads
Download.for_ip(#ip)
end
end
IpAddress.find(2130706433).downloads # SELECT * FROM downloads WHERE ip_address='2130706433'
This is totally a semantics issue, but this should probably change if you have no IpAddress table (i.e. how can we find the IpAddress object 2130706433 in the database if there is no IpAddress table - unless you make IpAddress a container rather than a specific single ipaddress, otherwise do something like instantiate new ones with a constructer like IpAddress(2130706433).downloads).
Otherwise, though, I don't see any problems in not having the IpAddress table. Why do you need it to be belongs_to, rather than just another column?
You can keep the models/objects if you wish to access them in similar ways:
class Download < ActiveRecord::Base
##Whatever Download-model-specific code you have...
def ip_address
#If nil, initialize new object. Return object.
#ip_address ||= IpAddress(ip_address_val)
end
end
class IpAddress
def initialize(address)
#value = address
end
def downloads
Download.where(:ip_address_val => self.value)
end
end
EDIT:
You can override the accessor, like you're asking. You just have to be careful in your code to be particular about what you're asking for.
See this doc: http://ar.rubyonrails.org/classes/ActiveRecord/Base.html
Under section "Overwriting default accessors"
Basically, if you do override the value, and if you wish to access the DB value, you use read_attribute(attr_name), so the code might look like this:
class Download < ActiveRecord::Base
##Whatever Download-model-specific code you have...
def ip_address
#If nil, initialize new object. Return object.
#ip_address ||= IpAddress(read_attribute(:ip_address))
end
end
class IpAddress
def initialize(address)
#value = address
end
def downloads
Download.where(:ip_address => self.value)
end
end
Though things might get a little confusing in your code if you aren't careful.
Add set_table_name "downloads" to your IpAddress and remove the relationship between the 2 coz it already has the column name ip_address.
This will give you queries in following way
Download.find(1).ip_address.to_s # '127.0.0.1'
Download.find(1).ip_address.to_i # 2130706433
IpAddress.find(Download.find(1).ip_address) # SELECT * FROM downloads WHERE ip_address='2130706433'
IpAddress.find(2130706433) # SELECT * FROM downloads WHERE ip_address='2130706433'

Mongoid: Querying from two collections and sorting by date

I currently have the following controller method in a Rails app:
def index
#entries = []
#entries << QuickPost.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries << Infographic.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries.flatten!.sort!{ |a,b| b.created_at <=> a.created_at }
#entries = Kaminari.paginate_array(#entries).page(params[:page]).per(10)
end
I realise this is terribly inefficient so I'm looking for a better way to achieve the same goal but I'm new to MongoDB and wondering what the best solution would be.
Is there a way to make a sorted limit() query or a MapReduce function in MongoDB across two collections? I'm guessing there isn't but it would certainly save a lot of effort in this case!
I'm currently thinking I have two options:
Create a master 'StreamEntry' type model and have both Infographic and QuickPost inherit from that so that both data types are stored on the same collection. The issue with this is that I have existing data and I don't know how to move it from the old collections to the new.
Create a separate Stream/ActivityStream model using something like Streama (https://github.com/christospappas/streama). The issues I can see here is that it would require a fair bit of upfront work and due to privacy settings and editing/removal of items the stream would need to be rebuilt often.
Are there options I have overlooked? Am I over-engineering with the above options? What sort of best practices are there for this type of situation?
Any info would be greatly appreciated, I'm really liking MongoDB so far and want to avoid falling into pitfalls like this in the future. Thanks.
The inherit solution is fine, but when the inherited models are close.
For example :
class Post < BasePost
field :body, type: String
end
class QuickPost < BasePost
end
class BasePost
field :title, type: String
field :created_at, type: Time
end
But when the models grows, or are too different, your second solution is better.
class Activity
include Mongoid::Document
paginates_per 20
field :occurred_at, :type => Time, :default => nil
validates_presence_of :occurred_at
belongs_to :user
belongs_to :quick_post
belongs_to :infographic
default_scope desc(:occurred_at)
end
and for example :
class QuickPost
include Mongoid::Document
has_one :activity, :dependent => :destroy
end
The dependant destroy make the activity destroyed when the QuickPost is destroyed. You can use has_many and adapt.
And to create the activities, you can create an observer :
class ActivityObserver < Mongoid::Observer
observe :quick_post, :infographic
def after_save(record)
if record.is_a? QuickPost
if record.new_record?
activity = record.build_activity
activity.user = record.user
# stuff when it is new
else
activity = record.activity
end
activity.occurred_at = record.occurred_at
# common stuff
activity.save
end
end
end

Proper way to do this with ActiveRecord?

Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.

accepts_nested_attributes_for :reject_if to trigger another method

I've got a multi-level nested form using formtastic_cocoon (jquery version of formtastic).
I am trying to do some validation in the sense of
if value is_numeric do
insert into database
else do
database lookup on text
insert id as association
end
I was hoping tha the accepts_nested_attributes_for would have an :if option, but apparently there is only the :reject_if.
Is there a way to create a validation like I describe as part of the accepts_nested_attributes_for??
-----------------------Updated as per Zubin's Response ---------------------------
I believe Zubin is on the right track with a method, but I can't seem to get it working just right. The method I am using is
def lookup_prereq=(lookup_prereq)
return if lookup_prereq.blank?
case lookup_prereq
when lookup_prereq.is_a?(Numeric) == true
self.task_id = lookup_prereq
else
self.task = Task.find_by_title(lookup_prereq)
end
end
When I trigger this function, the self.task_id is being put in the database as '0' rather than the Task.id.
I'm wondering if I'm missing something else.
I'm not completely sure that the method is actually being called. Shouldn't I need to say
lookup_prereq(attr[:prereq_id)
at some point?
-------------------further edit -----------------------
I think from what I can find that the method is called only if it is named with the same name as the value for the database, therefore I've changed the method to
def completed_task=(completed_task)
Unfortunately this is still resulting in 0 as the value in the database.
Sounds like you need a method in your nested model to handle that, eg:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author
def lookup_author=(lookup_author)
return if lookup_author.blank?
case lookup_author
when /^\d+$/
self.author_id = lookup_author
else
self.author = Author.find_by_name(lookup_author)
end
end
end

Resources