Rails 3: Privately Preview Model Changes - ruby-on-rails

I have a form for a model which accepts nested attrs for several other models:
class Page < ActiveRecord::Base
belongs_to :user
has_many :images
has_many :videos
has_many :options
accepts_nested_attributes_for :images
accepts_nested_attributes_for :videos
accepts_nested_attributes_for :options
def active?
published # boolean field
end
end
I'd like the page owner (user) to be able to edit the page and its nested attrs, and see those changes immediately without needing to save the model (which would make it publicly viewable). My gut reaction is to clone the Page along with all its associations (yikes!) so that the original stays intact until the owner is satisfied with the changes to the clone.
Is there a more sensible or efficient solution?

EDIT - After looking into has_draft which is option 2 - I would 100% go with that.
You have 3 options here:
Option 1 -
You can save the data into a session and retrieve it accordingly.
Option 2 -
You can use a draft plugin/gem like "has_draft" which will clone your model structure, create a new table, etc and handle all of it for you.
https://github.com/rubiety/has_draft
Option 3 -
(not sure if this will work exactly, but it's my best guess)
You can duplicate your page, add a new field to your page model called "duplicate_of" and when you hit your edit action, create a duplicate passing in id of the original page. Then in the update action check if it is a duplicate and if it is, overwrite the original and delete the duplicate.
def edit
#original = User.find(params[:user_id]).pages.find(params[:id])
#clone.duplicate_of = #original.id
#clone.active = false
#page = User.find(params[:user_id]).pages.create(#clone.attributes)
end
def update
#you will need to add something here to check if its a duplicate
#to begin with and if it is...
#page = User.find(params[:user_id]).pages.find(params[:id])
#original = User.find(params[:user_id]).pages.find(#page.duplicate_of)
if #original.update_attributes(params[:page])
#page.destroy
end
end

Related

Guidance on how to set validations correctly with a has_many :through relationship?

I've set up three models: User, List, and UserList -- the latter being the join model between User and List, in a has_many_through relationship.
I'm trying to set up what I think should be fairly vanilla uniqueness constraints -- but it's not quite working. Would appreciate your guidance / advice please!
Technical details
I have 3 models:
class User < ApplicationRecord
has_many :user_lists
has_many :lists, through: :user_lists, dependent: :destroy
End
class List < ApplicationRecord
has_many :user_lists
has_many :users, through: :user_lists, dependent: :destroy
# no duplicate titles in the List table
validates :title, uniqueness: true
End
class UserList < ApplicationRecord
belongs_to :list
belongs_to :user
# a given user can only have one copy of a list item
validates :list_id, uniqueness: { scope: :user_id }
end
As you can see, I'd like List items to be unique, based on their title. In other words, if user Adam adds a List with title "The Dark Knight", then user Beatrice adding a List with title "The Dark Knight" shouldn't actually create a new List record -- it should just create a new / distinct UserList association, pointing to the previously created List item.
(Somewhat tangential, but I also added a unique index on the table since I understand this avoids a race condition)
class AddIndexToUserLists < ActiveRecord::Migration[5.2]
def change
add_index :user_lists, [:user_id, :list_id], unique: true
end
end
Here's where things are going wrong.
As user Adam, I log in, and add a new title, "The Dark Knight", to my list.
Here's the controller action (assume current_user correctly retrieves Adam):
# POST /lists
def create
#list = current_user.lists.find_or_create_by!(list_params)
end
This correctly results in a new List record, and associated UserList record, being created. Hurrah!
As Adam, if I try to add that same title "The Dark Knight", to my list again, nothing happens -- including no errors on the console. Hurrah!
However -- as user Beatrice, if I log in and now try to add "The Dark Knight" to my list, I now get an error in the console:
POST http://localhost:3000/api/v1/lists 422 (Unprocessable Entity)
My debugging and hypothesis
If I remove the uniqueness constraint on List.title, this error disappears, and Beatrice is able to add "The Dark Knight" to her list.
However, List then contains two records, both titled "The Dark Knight", which seems redundant.
As Adam, it seems like perhaps current_user.lists.find_or_create_by!(list_params) in my controller action is finding the existing "The Dark Knight" list associated with my current user, and realising it exists -- thereby not triggering the create action.
Then as Beatrice, it seems that the same controller action is not finding the existing "The Dark Knight" list item associated with my current user -- and therefore it tries to trigger the create action.
However, this create action tries to create a new List item with a title that already exists -- i.e. it falls foul of the List.rb model uniqueness validation.
I'm not sure how to modify that find_or_create_by action, or the model validations, to ensure that for Beatrice, a new UserList record / association is created -- but not a new List record (since that already exists).
It feels like maybe I'm missing something easy here. Or maybe not. Would really appreciate some guidance on how to proceed. Thanks!
I'm 99% certain that what's happening is current_user.lists.find_or_create_by will only search for List records that the user has a UserList entry for. Thus if the List exists but the current user doesn't have an association to it, it will try to create a new list which will conflict with the existing one.
Assuming this is the issue, you need to find the List independently of the user associations: #list = List.find_or_create_by(list_params)
Once you have that list, you can create a UserList record through the associations or the UserList model. If you're looking for brevity, I think you can use current_user.lists << #list to create the UserList, but you should check how this behaves if the user has a UserList for that list already, I'm not sure if it will overwrite your existing data.
So (assuming the << method works appropriately for creating the UserList) your controller action could look like this:
def create
#list = List.find_or_create_by!(list_params)
current_user.lists << #list
end

How do I replace an ActiveRecord association member in-memory and then save the association

I have a has_may through association and I'm trying to change records in the association in memory and then have all the associations updated in a single transaction on #save. I can't figure out how to make this work.
Here's a simplifiction of what I'm doing (using the popular Blog example):
# The model
class Users < ActiveRecord::Base
has_many :posts, through: user_posts
accepts_nested_attributes_for :posts
end
# The controller
class UsersController < ApplicationController
def update
user.assign_attributes(user_params)
replace_existing_posts(user)
user.save
end
private
def replace_existing_posts(user)
user.posts.each do |post|
existing = Post.find_by(title: post.title)
next unless existing
post.id = existing
post.reload
end
end
end
This is a bit contrived. The point is that if a post that the user added already exists in the system, we just assign the existing post to them. If the post does not already exist we create a new one.
The problem is, that when I call user.save it saves any new posts (and the user_post association) but doesn't create the user_post association for the existing record.
I've tried to resolve this by adding has_many :user_posts, autosave: true to the User model, but despite the documented statement "When :autosave is true all children are saved", that doesn't reflect the behavior I see.
I can make this work, with something hacky like this, but I don't want to save the association records separately (and removing and replacing all associations would lead to lots of callback I don't want to fire).
posts = user.posts.to_a
user.posts.reset
user.posts.replace(posts)
I've trolled through the ActiveRecord docs and the source code and haven't found a way to add records to a has_many through association that create the mapping record in memory.
I finally got this to work, just by adding the association records manually.
So now my controller also does this in the update:
user.posts.each do |post|
next unless post.persisted?
user.user_posts.build(post: post)
end
Posting this as an answer unless someone has a better solution.

Ruby on Rails Association build and assign 2 related associations

So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly

Rails: Add an existing child as a new association to parent model via ajax

I try to add an existing child model to the parent via ajax, by only providing the new id.
My models:
class User < ActiveRecord::Base
has_many :books
attr_accessible :books
end
class Book < ActiveRecord::Base
belongs_to: :user
end
In my html form the user is viewing a User and it can select an existing Book to add. This will result in an Ajax request. I would like to only send the new Book, and not all the already assigned books. E.g. the User model has already Books 1 and 2, and now to user selects Book 3 to also be assigned.
I can not find the correct structure of the parameters. If I use the following, it completely overwrites the current associations.
// Ajax parameters
user[books] = [3]
How should is build the parameters such that it only adds the new book? And as a follow-up, how can I build the parameters to remove only a single association?
You have to send only one "book_id" in request.
Then in controller:
# assuming params hash is { :book_id => 3 }
#book = Book.find params[:book_id]
#user.books << #book
...
# Removing
#user.books.delete(#book)
# In `update` action
params[:user][:book_ids] = (#user.book_ids + params[:user][:book_ids]).flatten
#user.update_attributes(params[:user])

Rails: Manipulating params before saving an update - wrong approach?

First, I feel like I am approaching this the wrong way, but I'm not sure how else to do it. It's somewhat difficult to explain as well, so please bear with me.
I am using Javascript to allow users to add multiple text areas in the edit form, but these text areas are for a separate model. It basically allows the user to edit the information in two models rather than one. Here are the relationships:
class Incident < ActiveRecord::Base
has_many :incident_notes
belongs_to :user
end
class IncidentNote < ActiveRecord::Base
belongs_to :incident
belongs_to :user
end
class User < ActiveRecord::Base
has_many :incidents
has_many :incident_notes
end
When the user adds an "incident note", it should automatically identify the note with that particular user. I also want multiple users to be able to add notes to the same incident.
The problem I ran into is that when a user adds a new text area, rails isn't able to figure out that the new incident_note belongs_to the user. So it ends up creating the incident_note, but the user_id is nil. For example, in the logs I see the following insert statement when I edit the form and add a new note:
INSERT INTO "incident_notes" ("created_at", "updated_at", "user_id", "note", "incident_id") VALUES('2010-07-02 14:09:11', '2010-07-02 14:09:11', NULL, 'Another note', 8)
So what I've decided to try to do is manipulate the params for :incident in the update method. This way I can just add the user_id myself, however this seems un-rails-like, but I'm not sure how else to it.
When the form is submitted, the parameters look like this:
Parameters: {"commit"=>"Update", "action"=>"update", "_method"=>"put", "authenticity_token"=>"at/FBNxjq16Vrk8/iIscWn2IIdY1jtivzEQzSOn0I4k=", "id"=>"18", "customer_id"=>"4", "controller"=>"incidents", "incident"=>{"title"=>"agggh", "incident_status_id"=>"1", "incident_notes_attributes"=>{"1279033253229"=>{"_destroy"=>"", "note"=>"test"}, "0"=>{"id"=>"31", "_destroy"=>"", "note"=>"asdf"}}, "user_id"=>"2", "capc_id"=>"SDF01-071310-004"}}
So I thought I could edit this section:
"incident_notes_attributes"=>{"1279033253229"=>{"_destroy"=>"", "note"=>"test"}, "0"=>{"id"=>"31", "_destroy"=>"", "note"=>"another test"}}
As you can see, one of them does not have an id yet, which means it will be newly inserted into the table.
I want to add another attribute to the new item so it looks like this:
"incident_notes_attributes"=>{"1279033253229"=>{"_destroy"=>"", "note"=>"test", "user_id" => "2"}, "0"=>{"id"=>"31", "_destroy"=>"", "note"=>"another test"}}
But again this seems un-rails-like and I'm not sure how to get around it. Here is the update method for the Incident controller.
# PUT /incidents/1
# PUT /incidents/1.xml
def update
#incident = #customer.incidents.find(params[:id])
respond_to do |format|
if #incident.update_attributes(params[:incident])
# etc, etc
end
I thought I might be able to add something like the following:
params[:incident].incident_note_attributes.each do |inote_atts|
for att in inote_atts
if att.id == nil
att.user_id = current_user.id
end
end
end
But obviously incident_note_attributes is not a method. So I'm not sure what to do. How can I solve this problem?
Sorry for the wall of text. Any help is much appreciated!
I have a similar requirement and this is how I tackled it:
class Incident < ActiveRecord::Base
has_many :incident_notes
belongs_to :user
attr_accessor :new_incident_note
before_save :append_incident_note
protected
def append_incident_note
self.incident_notes.build(:note => self.new_incident_note) if !self.new_incident_note.blank?
end
end
and then in the form, you just use a standard rails form_for and use the new_incident_note as the attribute.
I chose this method because I knew it was just throwing data into the notes with minor data validations. If you have in depth validations, then I recommend using accepts_nested_attributes_for and fields_for. That is very well documented here.

Resources