In Rails 3, I am having a problem accessing a helper method from within a model
In my ApplicationController I have a helper method called current_account which returns the account associated with the currently logged in user. I have a project model which contains projects that are associated with that account. I have specified 'belongs_to :account' in my project model and 'has_many' in my account model. All of this seems to be working correctly and my projects are being associated with the right accounts.
However at the moment when I use 'Project.all', all of the project records are being returned as you would expect. I would like it to automatically filter the projects so that only those associated with the specified account are returned and I would like this to be the default behaviour
So I tried using 'default_scope'. The line in my model looks something like this
default_scope :order => :name, :conditions => ['account_id = ?', current_account.id]
This throws an error saying
Undefined local variable or method current_account for #
If I swap the call to current_account.id for an integer id - eg
default_scope :order => :name, :conditions => ['account_id = ?', 1]
Everything works correctly. How do I make my current_account method accessible to my model
Many Thanks
You can't access the session from models. Instead, pass the account as a parameter to a named scope.
#controller
Model.my_custom_find current_account
#model.rb
named_scope :my_custom_find, lambda { |account| { :order => :name, :conditions => ['account_id = ?', account.id]}}
I haven't used rails 3 yet so maybe named_scopes have changed.
The association setup is enough to deal with scoping on controller and view levels. I think the problem is to scope, for instance, finds in models.
In controller:
#products = current_account.products.all
Fine for view scoping, but...
In model:
class Inventory < ActiveRecord::Base
belongs_to :account # has fk account_id
has_many :inventory_items, :dependent => :destroy
has_many :products, :through => :inventory_items
end
class Product < ActiveRecord::Base
has_many :inventory_items
def level_in(inventory_id)
inventory_items.where(:inventory_id => inventory_id).size
end
def total_level
# Inventory.where(:account_id => ?????) <<<<<< ISSUE HERE!!!!
Inventory.sum { |inventory| level_in(inventory.id) }
end
end
How can this be scoped?
+1 for mark's answer. This should still work in Rails 3.
Just showing you the rails 3 way with scope and new query api:
scope :my_custom_find, lambda { |account| where(:account_id=>account.id).order(:name) }
You've got the association set up, so couldn't you just use:
#projects = current_account.projects.all
...in your controller?
I've not adjusted the syntax to be Rails 3 style as I'm still learning it myself.
Related
I have a class like this:
class Project < ActiveRecord::Base
has_many :versions, :order => :position
end
In a controller action, I need to query for a project's versions. I have the project ID. I could simply do a
versions = Project.find(params[:project_id]).versions
However, this will first do a query to the "projects" table, which is unnecessary in my case. I could change it to
versions = Version.find( :conditions => { :project_id => params[:project_id } )
(or use a dynamic finder with the same results)
This doesn't use the additional attributes of the association (in this case, the ":order => :position"), so if I wanted to get exactly the same result as in the same example, I will have to write:
versions = Version.find( :conditions => { :project_id => params[:project_id },
:order => :position )
And here I'm no longer DRY since I'm repeating something (the ":order" option) that is already defined in the model.
Is there a way to use and follow the association defined in the model without having to load the parent object? I'm currently still using Rails 2.3.8.
I would recommend to use one default_scope for ordering and another scope to find versions by project in Version model.
Project
class Project < ActiveRecord::Base
has_many :versions
end
Version
class Version < ActiveRecord::Base
belongs_to :project
named_scope :order_by_position, {:order => "position"}
named_scope :by_project_id, (lambda do |project_id|
{:conditions => ['project_id = ?', project_id]}
end)
end
Then you can use:
versions = Version.by_project_id(params[:project_id])
Sorry but I'm pretty sure you cannot do that - at least not following the public ActiveRecord API.
You could write extension that uses internal ActiveRecord API to inspect association options, but I wouldn't recommend this.
You could reuse some conditions through scopes but since you're still on Rails 2.3 that is not helping you.
I think your best bet is manually fetching version instances as you have written in the last example:
versions = Version.find( :conditions => { :project_id => params[:project_id },
:order => :position )
EDIT: What you could do in Rails4:
You can reuse and share scopes between models and associations like this:
class Project < ActiveRecord::Base
has_many :versions, -> { ordered }
end
class Version
def self.ordered
order(:position)
end
end
versions = Version.find_by(:project_id, params[:project_id]).ordered
The more scopes and the more complicated they are, the more useful this approach is.
Imagine the scenario...
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
end
#models/account.rb
class Account < ActiveRecord::Base
belongs_to :user
def public_accounts
Account.all :conditions => { public => true }
end
end
Now imagine I want to concatenate User(:id).accounts with Account.public_accounts to show a list of all accounts available to a user.
So you'd think I'd be able to update the User model to look like this.
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
def all_accounts
self.accounts + Account.public
end
end
However, now I won't be able to use the all() method since it's no longer of that type of object.
In the controller I'd like to do this...
#controllers/accounts_controller.rb
def search_all
User.find(params[:user_id]).all_accounts.all(
:offset => params[:offset],
:limit => params[:limit]
)
end
Thoughts?
Update #1:
Scope's won't work for my scenario. I simplified my scenario to try and get my point across. As stated I need a way to combine two active record results and retain the ability to further filter them in my controller.
So the question is, "Why?" The reason is, I am trying to combine two sets of records to form a complete collection and one of the collections is not associated with the user at all.
I have refactored the above scenario to try and show a more precise example without getting overly complicated.
This might be a good scenario to use scopes.
You can define active and inactive scopes in the Account model and then use the following:
User.accounts
User.accounts.active
User.accounts.inactive
You can even chain scopes together, so you could do something like:
User.accounts.active.paid_up
jklina's answer is correct, it's best to use scopes in this situation. Scopes provide a sugary syntax and are more readable. I'll elaborate on the setup:
class User < AR::Base
has_many :accounts
end
class Account < AR::Base
belongs_to :user
scope :active, where(:active => true)
scope :inactive, where(:active => false)
end
You would then access the account scopes as jklina showed: User.find(1).accounts.active, etc.
Accessing all of a user's accounts like: User.find(1).accounts.
UPDATE:
I fixed some mistakes and added more below.
Based on the updates to your question, I think you need to make the public method a method on the class:
class Accounts < AR::Base
...
# This is essentially a scope anyways
def self.public
where(:public => true)
end
end
class User < AR::Base
...
# This should return all the users accounts
# and any public accounts
def all_accounts
Account.where("user_id = ? OR public is true", self.id)
end
end
Lets look at the return values in the chain:
User.find(params[:user_id]) # returns an instance of User class
User.find(params[:user_id]).all_accounts # returns an array
The Array class doesn't have an instance method called all that's why you are seeing this error. This is not a bug.
Why don't you try this:
class User
has_many :accounts, :conditions => { :active => 1 }
has_many :all_accounts :conditions => ["(active = ? OR public = ?)",
true, true]
end
Now you can:
User.find(params[:user_id]).all_accounts.all(:limit => 10, :offset => 2)
You're try to access two distinct tables and apply LIMIT/OFFSET to them as a combined union. That aint gonna happen unless you logically combine them at the SQL layer, not at the ActiveRecord layer.
Sounds like writing out the SQL, maybe using a UNION and then using find_by_sql might be your best best.
Given the following models:
class Company
include Mongoid::Document
has_many :workers, autosave: true
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
class Worker
include Mongoid::Document
field :hours
attr_accessible :hours
belongs_to :company
end
class Manager < Worker
field :order
has_many :contributors, :class_name => "Worker"
attr_accessible :order, :contributors
end
class Contributor < Worker
field :task
belongs_to :manager, :class_name => "Worker"
attr_accessible :task
end
How does one create a manager in a company in the controller and view using nested attributes?
Here's my guess:
def new
#company = Company.new
#company.workers = [Manager.new]
end
def create
#company = Company.new params[:user]
if #company.save
redirect_to root_url, :notice => "Company with manager created."
else
render :new
end
end
= semantic_form_for #company do |f|
= f.semantic_fields_for :workers do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
problem is the order field which specifically belongs to the manager is not persisting after the create. Also when the data is improperly filled there is an error:
undefined method `order' for #<Worker:0x0000000646f018> (ActionView::Template::Error)
So is there a way for nested attributes to handle inheritance in the models from mongoid?
The question is related to Can nested attributes be used in combination with inheritance? except instead of active record using mongoid.
Honestly, this is a paraphrasing of my code... the real code is more complex situation although i believe these are all of the relevant parts. If you have more questions ask.
UPDATE:
I changed the view to the following:
= semantic_form_for #company do |f|
- #company.workers.each do |worker|
- if worker._type == "Manager"
= f.semantic_fields_for :workers, worker do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
I do not get the error anymore, however the nested attributes do not update the company object properly. The params are the following:
{"company"=> {"workers_attributes"=>{"0"=>{"hours"=>"30", "order" => "fish", "id"=>"4e8aa6851d41c87a63000060"}}}}
Again edited for brevity. So the key part is that there is a hash between "0" => {data for manager}. The workers data seems to be held in a hash. I would expect the data to look more like the following:
params = { company => {
workers_attributes => [
{ hours => "30", "order" => "fish" }
]}}
This is different because the workers data is held in an array instead of a hash. Is there another step to get the nested attributes to save properly?
Thanks
what version of Mongoid are you using? Because I don't think the use of refereneces_many is encouraged -- Not that that's related to your problem here, just wanted to probe what version you're using. In the doc on the gorgeous Mongoid.org, get this, I had to learn it the hard way, they say for Updating your records, you need to the autossave set to true. That's NOT accurate. You need it for even creating
so:
class Company
include Mongoid::Document
has_many :workers, :autossave => true # your money shot
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
ADDED:
I was re-reading your code, I spotted the following that might be the problem: Your Company model is set to has_many :workers and is set to accept nested attribbutes for Worker when changes come in, correct? And there is a field named Order in your Manager model which is subclassed from Worker. Yet you're having a form whose nested fields part is pointed at Worker not at Manager, the model that actually has the Order field. And that's obviously not enough, because Company isn't having_many :managers yet, you may need to set it to has_many :managers in the Company model as well.
I'm implementing a method, that will be used in many places of a project.
def do association
end
"association" is a symbol, like :articles, :tags, :users etc.
When the association is :articles, I need to work with the Article model.
When the association is :users, I need to work with the User model.
Etc.
I know, that I can write a helper method, that returns model class, depending on the provided symbol. But is there a ready to use method for that?
Rails provides a method called classify on the String class for such purpose.
:users.to_s.classify.constantize
#User
:line_items.to_s.classify.constantize
#LineItem
Edit:
If you are trying to retrieve the class associated with an association, use this approach:
Author.reflect_on_association(:books).klass
# => Book
This will address the scenario where the association name doesn't match the class name.
E.g:
class Order
has_many :line_items
has_many :active_line_items, :class_name => "LineItem",
:conditions => {:deleted => false}
end
In the example above, :active_line_items will result in ActiveLineItem and our original code will throw error.
Read more about this here.
This will work
(:users.to_s.singularize.capitalize.constantize).find :all, :conditions => ["name = ?", "john"]
And with your example
association.to_s.singularize.capitalize.constantize
If I have something like this:
class Post < ActiveRecord::Base
has_many :comments, :as => :commentable do
def approved
find(:all, :conditions => {:approved => true})
end
end
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
...when I do this, I get 2 hits to the database (not including finding the post :p):
post = Post.first
post.comments #=> [Comment1, Comment2...]
post.comments.approved #=> [Comment1, Comment7...]
It seems like it should just filter the current comments array in memory, no? Is it doing that? Reason I ask is because the console shows SELECT * FROM ... on post.comments.approved, even though I already called post.comments. Shouldn't this be better optimized in ActiveRecord?
The AR executes a new query for any finder calls inside a association extension method.
You can refer to the cached result set by using self.
has_many :comments, :as => :commentable do
def approved
# uses the cached result set
self.select{|c| c.approved == true}
end
end
It's optional, as in some cases you might only want to load the associated objects when needed. If you want them all loaded into memory, you need to explicitly declare the objects you'd like included with the initial query, using the :include flag. Example:
post = Post.find(:first, :include => :comment)
You might have to rewrite your extension to take advantage of the feature... an approach would be to change your "approved" function to iterate through the comments array attached to each post, and return a new array with the nonapproved comments filtered out. The "find" you have defined explicitly goes back to the database.
If your query is really that simple, then what you want is a named scope:
class Comment
named_scope :approved, :conditions => {:approved => true}
end
Then, you can do:
#post.comments.approved.count #=> 1 DB hit!
#post.comments.count #=> Another DB hit - can't reuse same scope
Look at #scope (#named_scope in Rails 2.3).