accept_nested_attributes_for in a many-to-many relationship - ruby-on-rails

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. :)

Related

Rails: Block building/create for model

I have a simple e-commerce application with models User and Order. The model Order has a column status indicating the status of the order where an order with status = 0 is a cart. In practice, User can have many Orders. a User can have only one cart though. To achieve this functionality, I have the following models. order.rb:
STATUS_VALUES = { 'CART' => 0, 'CONFIRMED' => 1,'DELIVERED' => 2}
class Order < ApplicationRecord
belongs_to :user
has_many :order_items, inverse_of: :order
accepts_nested_attributes_for :order_items
end
user.rb:
class User < ApplicationRecord
has_secure_password
has_many :photos
has_many :orders
has_one :cart
end
And in addition I have created the model Cart, where cart.rb:
class Cart < Order
self.table_name = "orders"
default_scope { where("orders.status = 0") }
end
Through this implementation, I'm able to:
1. For User to allow only building one cart at a time. Two successive user.build_cart won't work. This is perfect, exactly what I wanted. My problem though is that you can user.orders.build(status: 0) will allows work no matter how many carts I already have.
My aim is to block creating orders so that building carts is only allowed. A cart can then be updated to a non-cart through changing the status column. How can I do this in Rails?
The best way to build cart is to save them in session variables not in databases. U can find solutions by doing quick search.

Updating record with join table creates duplicate entries

I have 3 models as follows:
class Document < ActiveRecord::Base
has_many :documents_tasks, inverse_of: :document
has_many :tasks, through: :documents_tasks, dependent: :destroy
end
class Task < ActiveRecord::Base
has_many :documents_tasks, inverse_of: :task
has_many :documents, through: :documents_tasks, dependent: :destroy
end
class DocumentsTask < ActiveRecord::Base
belongs_to :task, inverse_of: :documents_tasks
belongs_to :document, inverse_of: :documents_tasks
validates_uniqueness_of :document_id, scope: :task_id
end
In the above when I try to update the record for a Task it throws a validation error for duplicate entries on the DocumentsTask model if I keep the validation or directly inserts duplicates if remove the validation.
My code to update the Task record is:
def update
#task = #coach.tasks.find(params[:id])
#task.update(:name => task_params[:name], :description => task_params[:description] )
#task.documents << Document.find(task_params[:documents])
if #task.save
render 'show'
else
render status: 500, json: {
error: true,
reason: #task.errors.full_messages.to_sentence
}
end
end
I know I can add unique index to the db to automatically prevent duplicate entries but is there some way I can prevent the controller from updating the join table values when they're the same?
So when I attempt to update the associated documents, ex:
I had document 5 initially
Now I add document 6 and call the update function
It attempts to re-add both documents 5 and 6 to the db so I get the error:
Completed 422 Unprocessable Entity in 9176ms
ActiveRecord::RecordInvalid (Validation failed: Document has already been taken)
This is because I added the following validation:
validates_uniqueness_of :document_id, scope: :task_id
in my DocumentsTask model as shown above. The issue is how can I prevent it from attempting to re-add existing records
Assuming that task_params[:documents] is an array of document ids (based on how you're using it with find now), you should be able to do something like this as a quick fix:
#task.documents << Document.where(id: task_params[:documents]).where.not(task_id: #task.id)
Basically this just filters out the documents that are already associated to the given task before assigning them to the task.
That said, I'd suggest something more robust as a long term solution. A couple of options (among many) would be extracting the responsibility of task creation out into it's own class (so you can more easily test it and make that functionality more portable), or you could look into overriding the setter method(s) for documents in your task model similar to what this answer describes: https://stackoverflow.com/a/2891245/456673

rails join table issue with different roles (owner, non-owner)

In my app users can create products so at the moment User has_many :products and Product belongs_to :user. Now I want the product creator product.user to be able to invite other users to join the product, but I wanna keep the creator the only one who can edit the product.
One of the setups I've got in my mind is this, but I guess it wouldn't work, since I don't know how to distinguish between created and "joined-by-invitation" products when calling user.products.
User
has_many :products, through: :product_membership
has_many :product_memberships
has_many :products # this is the line I currently have but think it wouldn't
# work with the new setup
Product
has_many :users, through: :product_membership
has_many :product_memberships
belongs_to :user # I also have this currently but I'd keep the user_id on the product
# table so I could call product.user and get the creator.
ProductUsers
belongs_to :user
belongs_to :product
Invitation
belongs_to :product
belongs_to :sender, class: "User"
belongs_to :recipient, class: "User"
To work around this issue I can think of 2 solutions:
Getting rid of the User has_many :products line that I currently have and simply adding an instance method to the user model:
def owned_products
Product.where("user_id = ?", self.id)
end
My problem with this that I guess it doesn't follow the convention.
Getting rid of the User has_many :products line that I currently have and adding a boolean column to the 'ProductUsers' called is_owner?. I haven't tried this before so I'm not sure how this would work out.
What is the best solution to solve this issue? If none of these then pls let me know what you recommend. I don't wanna run into some issues later on because of my db schema is screwed up.
You could add an admin or creator attribute to the ProductUsers table, and set it to false by default, and set it to true for the creator.
EDIT: this is what you called is_owner?
This seems to be a fairly good solution to me, and would easily allow you to find the creator.
product.product_memberships.where(is_owner?: true)
should give you the creator

Rails ActiveRecord includes with run-time parameter

I have a few models...
class Game < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :game
belongs_to :voter, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :games, dependent: :destroy
has_many :votes, dependent: :destroy
end
In my controller, I have the following code...
user = User.find(params[:userId])
games = Game.includes(:manager, :votes)
I would like to add an attribute/method voted_on_by_user to game that takes a user_id parameter and returns true/false. I'm relatively new to Rails and Ruby in general so I haven't been able to come up with a clean way of accomplishing this. Ideally I'd like to avoid the N+1 queries problem of just adding something like this on my Game model...
def voted_on_by_user(user)
votes.where(voter: user).exists?
end
but I'm not savvy enough with Ruby/Rails to figure out a way to do it with just one database roundtrip. Any suggestions?
Some things I've tried/researched
Specifying conditions on Eager Loaded Associations
I'm not sure how to specify this or give the includes a different name like voted_on_by_user. This doesn't give me what I want...
Game.includes(:manager, :votes).includes(:votes).where(votes: {voter: user})
Getting clever with joins. So maybe something like...
Game.includes(:manager, :votes).joins("as voted_on_by_user LEFT OUTER JOIN votes ON votes.voter_id = #{userId}")
Since you are already includeing votes, you can just count votes using non-db operations: game.votes.select{|vote| vote.user_id == user_id}.present? does not perform any additional queries if votes is preloaded.
If you necessarily want to put the field in the query, you might try to do a LEFT JOIN and a GROUP BY in a very similar vein to your second idea (though you omitted game_id from the joins):
Game.includes(:manager, :votes).joins("LEFT OUTER JOIN votes ON votes.voter_id = #{userId} AND votes.game_id = games.id").group("games.id").select("games.*, count(votes.id) > 0 as voted_on_by_user")

ActiveRecord: Access join table attribute through association using delegate

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

Resources