ActiveRecord: Access join table attribute through association using delegate - ruby-on-rails

I have a Post model in my app, which has a posts attribute that stores a JSON object that looks something like this:
Post.last.posts = {
twitter: 'This is a tweet',
facebook: 'This is a facebook post'
}
A user creates a single Post, which is then sent out to multiple platforms. This architecture made sense for the original app design. Now I want to offer the user the ability to hide a post they've made to one platform without affecting posts to other platforms. Because a post is a single model in the database I need to find a workaround. I'm not sure if this is the best approach, but I decided to create a join table between my User model and Post model. Note that posts are created by a different user model (Admin) and User merely views posts.
Here are my models:
class Post < ActiveRecord::Base
has_many :post_users
has_many :users, through: :post_users
end
class User < ActiveRecord::Base
has_many :post_users
has_many :posts, through: :post_users
end
class PostUser < ActiveRecord::Base
belongs_to :post
belongs_to :user
delegate :hide_twitter, to: :post
delegate :hide_facebook, to: :post
def hide_twitter
self.hide_twitter
end
end
In the view I'm building, each Post is represented as a series of cards - one for each platform. So if a post is on Twitter and Facebook, it will be shown as two seperate cards - one per platform. What I want to do is give User the ability to hide one of the cards without affecting the other(s). Because a Post belongs to many users, this has to be an attribute of a join table (e.g. PostUser).
What I'd like to know is if it's possible to access this attribute of the join table through the Post model? I want to do something like the following but I'm not sure if it's possible or if I'm taking the correct approach by using delegate in my join table.
current_user.posts.first.hide_twitter
=> false
current_user.posts.first.hide_facebook
=> true
When I use delegate as above and try to call the above line of code, I get the following error:
Post Load (1.4ms) SELECT "posts".* FROM "posts" INNER JOIN "post_users" ON "posts"."id" = "post_users"."post_id" WHERE "post_users"."user_id" = $1 ORDER BY "posts"."id" ASC LIMIT 1 [["user_id", 90]]
NoMethodError: undefined method `hide_twitter' for #<Post:0x007fc27d383f50>
from /Users/ACIDSTEALTH/.gem/ruby/2.3.0/gems/activemodel-4.2.5.1/lib/active_model/attribute_methods.rb:433:in `method_missing'
I realize I could do something very roundabout like this answer, but was hoping for something a little more elegant/conventional.

How about something like:
class Post < ActiveRecord::Base
has_many :post_users
has_many :users, through: :post_users
end
class User < ActiveRecord::Base
has_many :post_users
has_many :posts, through: :post_users
end
class PostUser < ActiveRecord::Base
belongs_to :post
belongs_to :user
end
And then:
PostUser.where(post: current_user.posts.first).hide_twitter

Related

Rails: Query to get collection associated with collection

So I have these four classes:
class User < ActiveRecord::Base
has_many :water_rights
end
class WaterRight < ActiveRecord::Base
belongs_to :user
has_many :place_of_use_area_water_rights
has_many :place_of_use_areas, through: :place_of_use_area_water_rights
end
class PlaceOfUseAreaWaterRight < ActiveRecord::Base
belongs_to :place_of_use_area
belongs_to :water_right
end
class PlaceOfUseArea < ActiveRecord::Base
has_many :place_of_use_area_water_rights
has_many :water_rights, through: :place_of_use_area_water_rights
end
and I call User.first.water_rights and get a collection of WaterRights. My question is how do I get a collection of PlaceOfUseAreas associated with those WaterRights without doing something like this:
areas = []
water_rights.each do |wr|
areas << wr.place_of_use_areas
end
areas.flatten.uniq{ |a| a.id }
This works but it makes a new query for every single WaterRight. I'm looking for a way to make one query to get the collection of associated PlaceOfUseAreas.
You just want to get all associated PlaceOfUseAreas objects in single query, right?
If so, Rails have pretty single line solution for it:
PlaceOfUseArea.joins(:water_wights).uniq
Read more about joins method if you want more information.

Can I use as act-as-list with a polymorphic association?

I am working on a liquid democracy app. My data model is:
Both Users and Organizations are Voters. A Voter has an ordered list of Delegations. A Delegation consists of a set of Tags and an ordered list of Voters (delegates).
When determining which Voter to serve as proxy, the first Delegation with a matching Tag is used and then the first delegate in the list who has voted on the Issue.
To create the ordered list of delegations, I can simply add a position field to the Delegation model. acts_as_list can then be used to manage the order.
What I'm uncertain about is how to structure the list of delegates. It seems like the database columns should be:
delegation_id
delegate_type
delegate_id
position
My current stab is:
class User < ActiveRecord::Base
has_and_belongs_to_many :organizations
has_many :delegations, as: :voter
acts_as_tagger
acts_as_voter
end
class Organization < ActiveRecord::Base
has_and_belongs_to_many :users
has_many :delegations, as: :voter
acts_as_tagger
acts_as_voter
end
class Delegation < ActiveRecord::Base
belongs_to :voter, polymorphic: true
has_many :tags
has_and_belongs_to_many :delegation_entries, -> { order("position ASC") }
acts_as_list scope: :voter
end
class DelegationEntry < ActiveRecord::Base
belongs_to :delegation
acts_as_list scope: :delegation
end
When I do Organization.first.delegations << Delegation.create from the console I see:
(0.4ms) BEGIN
Delegation Load (1.0ms) SELECT "delegations".* FROM "delegations" WHERE ("delegations"."voter_id" IS NULL AND position > 1) ORDER BY delegations.position ASC LIMIT 1
SQL (0.7ms) UPDATE "delegations" SET position = (position + 1) WHERE ("delegations"."voter_id" = 1 AND position >= 1)
So, acts_as_list doesn't handle polymorphic associations apparently. The increment is only on the id, so it would update both the User and Organization. Since order is preserved, does this matter? It would likely become an issue when doing reorderings and insertions.
Ultimately, this structure was replaced with single table inheritance which has a single set of identifiers, so acts_as_list will work.

Retrieve data from join table

I am new in RoR and I am trying to write a query on a join table that retrieve all the data I need
class User < ActiveRecord::Base
has_many :forms, :through => :user_forms
end
class Form < ActiveRecord::Base
has_many :users, :through => :user_forms
end
In my controller I can successfully retrieve all the forms of a user like this :
User.find(params[:u]).forms
Which gives me all the Form objects
But, I would like to add a new column in my join table (user_forms) that tells the status of the form (close, already filled, etc).
Is it possible to modify my query so that it can also retrieve columns from the user_forms table ?
it is possible. Once you've added the status column to user_forms, try the following
>> user = User.first
>> closed_forms = user.forms.where(user_forms: { status: 'closed' })
Take note that you don't need to add a joins because that's taken care of when you called user.forms.
UPDATE: to add an attribute from the user_forms table to the forms, try the following
>> closed_forms = user.forms.select('forms.*, user_forms.status as status')
>> closed_forms.first.status # should return the status of the form that is in the user_forms table
It is possible to do this using find_by_sql and literal sql. I do not know of a way to properly chain together rails query methods to create the same query, however.
But here's a modified example that I put together for a friend previously:
#user = User.find(params[:u])
#forms = #user.forms.find_by_sql("SELECT forms.*, user_forms.status as status FROM forms INNER JOIN user_forms ON forms.id = user_forms.form_id WHERE (user_forms.user_id = #{#user.id});")
And then you'll be able to do
#forms.first.status
and it'll act like status is just an attribute of the Form model.
First, I think you made a mistake.
When you have 2 models having has_many relations, you should set an has_and_belongs_to_many relation.
In most cases, 2 models are joined by
has_many - belongs_to
has_one - belongs_to
has_and_belongs_to_many - has_and_belongs_to_many
has_and_belongs_to_many is one of the solutions. But, if you choose it, you must create a join table named forms_users. Choose an has_and_belongs_to_many implies you can not set a status on the join table.
For it, you have to add a join table, with a form_id, a user_id and a status.
class User < ActiveRecord::Base
has_many :user_forms
has_many :forms, :through => :user_forms
end
class UserForm < ActiveRecord::Base
belongs_to :user
belongs_to :form
end
class Form < ActiveRecord::Base
has_many :user_forms
has_many :users, :through => :user_forms
end
Then, you can get
User.find(params[:u]).user_forms
Or
UserForm.find(:all,
:conditions => ["user_forms.user_id = ? AND user_forms.status = ?",
params[:u],
'close'
)
)
Given that status is really a property of Form, you probably want to add the status to the Forms table rather than the join table.
Then when you retrieve forms using your query, they will already have the status information retrieved with them i.e.
User.find(params[:u]).forms.each{ |form| puts form.status }
Additionally, if you wanted to find all the forms for a given user with a particular status, you can use queries like:
User.find(params[:u]).forms.where(status: 'closed')

accept_nested_attributes_for in a many-to-many relationship

I have tried to find a solution for this but most of the literature around involves how to create the form rather than how to save the stuff in the DB. The problem I am having is that the accepts_nested_attributes_for seems to work ok when saving modifications to existing DB entities, but fails when trying to create a new object tree.
Some background. My classes are as follows:
class UserGroup < ActiveRecord::Base
has_many :permissions
has_many :users
accepts_nested_attributes_for :users
accepts_nested_attributes_for :permissions
end
class User < ActiveRecord::Base
has_many :user_permissions
has_many :permissions, :through => :user_permissions
belongs_to :user_group
accepts_nested_attributes_for :user_permissions
end
class Permission < ActiveRecord::Base
has_many :user_permissions
has_many :users, :through => :user_permissions
belongs_to :user_group
end
class UserPermission < ActiveRecord::Base
belongs_to :user
belongs_to :permission
validates_associated :user
validates_associated :permission
validates_numericality_of :threshold_value
validates_presence_of :threshold_value
default_scope order("permission_id ASC")
end
The permission seem strange but each of them has a threshold_value which is different for each user, that's why it is needed like this.
Anyway, as I said, when I PUT an update, for example to the threshold values, everything works ok. This is the controller code (UserGroupController, I am posting whole user groups rather than one user at a time):
def update
#ug = UserGroup.find(params[:id])
#ug.update_attributes!(params[:user_group])
respond_with #ug
end
A typical data coming in would be:
{"user_group":
{"id":3,
"permissions":[
{"id":14,"name":"Perm1"},
{"id":15,"name":"Perm2"}],
"users":[
{"id":7,"name":"Tallmaris",
"user_permissions":[
{"id":1,"permission_id":14,"threshold_value":"0.1"},
{"id":2,"permission_id":15,"threshold_value":0.3}]
},
{"name":"New User",
"user_permissions":[
{"permission_id":14,"threshold_value":0.4},
{"permission_id":15,"threshold_value":0.2}]
}]
}
}
As you can see, the "New User" has no ID and his permission records have no ID either, because I want everything to be created. The "Tallmaris" user works ok and the changed values are updated no problem (I can see the UPDATE sql getting run by the server); on the contrary, the new user gives me this nasty log:
[...]
User Exists (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."name" = 'New User' LIMIT 1
ModelSector Load (8.7ms) SELECT "user_permissions".* FROM "user_permissions" WHERE (user_id = ) ORDER BY permission_id ASC
PG::Error: ERROR: syntax error at or near ")"
The error is obviously the (user_id = ) with nothing, since of course the user does not exists, there are no user_permissions set already and I wanted them to be created on the spot.
Thanks to looking around to this other question I realised it was a problem with the validation on the user.
Basically I was validating that the threshold_values summed up within certain constraints but to do that I was probably doing something wrong and Rails was loading data from the DB, which was ok for existing values but of course there was nothing for new values.
I fixed that and now it's working. I'll leave this here just as a reminder that often a problem in one spot has solutions coming from other places. :)

Making record available only to certain models in Rails 3

I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end

Resources