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

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.

Related

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

How do you associate one model with another?

I have a model called Topic and another called Product.
Here's how the Topic model looks,
class Topic < ActiveRecord::Base
belongs_to :product
end
Topic has columns "title" and "body".
And here's Product,
class Product < ActiveRecord::Base
has_many :topics
end
Product has columns "name" and "desc". Name is unique.
When I create a new Topic, I want the title of Topic to be stored in Name of Product, only if Name doesn't exist yet. If it does, it should not make any change.
But how do I do this?
UPDATE:
User comes to /topics page, enters Title and Body.
What the Topics_controller should do,
1. Read the Title that has been given by the user.
2. Check if that Title already exists in the Products.
3. If it doesn't add it.
4. If it does, don't do anything.
I don't understand how these two models are linked together and how I can access records from the model.
You can achieve this by using one of the callbacks, which ActiveRecord provides.
I'm not sure if I understand your requirements perfectly, so maybe you need to alter the following example:
class Topic < ActiveRecord::Base
belongs_to :product
before_save :add_product_name
private
def add_product_name
self.product.name ||= self.title if Product.find_by(name: self.title).nil?
end
end
You can write a callback like before_create :add_topic_name
Where that method will find product name of topic and assign it to the title of product.
Your requirements are a bit unclear here. Can you specify what your end goal is, from a Behaviour point of view?
If i understand correctly though, why not just overwrite the title method for Topic. This way you are not duplicating data in the DB.
within your Topic class:
class Topic < ActiveRecord::Base
def title
read_attribute(:title) || product.name # will get title of #topic if not nil, else product name
end
end

Rails 3: Privately Preview Model Changes

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

How to I make my custom mySQL query to in rails 3?

Im trying to display recently added comments from tattoos a user has posted. So If I posted a tattoo, and then user_b posted "hey I like your tattoo" then Im trying to get just the comment.
First of all Im using the acts_as_commentable_with_threading gem which doesnt create a foreign key for the table im trying to join. So my controller cant look for tattoo_id, it has to look for commentable_id
In the controller I would have to call the Comment model and then pass some SQL stuff into it but apparently I have no clue how to pass custom SQL queries into ruby because even tho my query string works in terminal, I get all sorts of nonsense when trying to use it in rails.
Im basically trying to do this:
SELECT comments.id FROM comments,tattoos WHERE commentable_id = tattoos.id AND
tattoos.member_id = #{current_user}
where #{current_user} will be the current_user passed in.
You don't have to jump through so many hoops to accomplish this. acts_as_commentable assigns a polymorphic association, so you should set it up like this:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :commentable, :polymorphic => true
end
class Tattoo < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class User
has_many comments
end
Then you can access the association as usual:
Tattoo.where(:member_id => current_user).first.comments
See http://railscasts.com/episodes/154-polymorphic-association for a general tutorial on how polymorphic associations work. It just so happens that this railscast uses exactly :commentable as the polymorphic example, so you should be able to follow along directly if you want.
I think Ben's approach is best but for future reference if you do come across something more complicated you can always use sql for example:
Comment.find_by_sql("SELECT comments.* FROM comments,tattoos WHERE commentable_id = tattoos.id AND tattoos.member_id = ?", current_user)

How does one intercept nested_attributes for further processing?

tl;dr: Is it possible to intercept posted values from a nested model for further processing? I've tried everything I can think of to access the nested attributes to use for a before_save callback, but that may be only a testament to the limits of my imagination.
I'm writing a library application, where books can have many authors and vice versa. That bit is working just fine with a has_many_through and accepts_nested_attributes_for. The app saves all the book and author information just fine. Except... I can't seem to write a working before_save on either the Author or Book model to check if the Author that we're trying to create exists. Here's what I already have:
class Book < ActiveRecord::Base
has_many :authorships
has_many :authors, :through => :authorships
accepts_nested_attributes_for :authors, :authorships
end
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, :through => :authorships
before_save :determine_distinct_author
def determine_distinct_author
Author.find_or_create_by_author_last( #author )
end
end
## books_controller.rb ##
def new
#book = Book.new
#book.authors.build
respond_to do |format|
format.html
format.xml { render :xml => #book }
end
end
def create
#book = Book.new(params[:book])
#author = #book.authors #I know this line is wrong. This is the problem (I think.)
# more here, obviously, but nothing out of ordinary
end
When I post the form, the dev log passes on this:
Parameters: {"commit"=>"Create Book",\
"authenticity_token"=>"/K4/xATm7eGq/fOmrQHKyYQSKxL9zlVM8aqZrSbLNC4=",\
"utf8"=>"✓", "book"{"title"=>"Test", "ISBN"=>"", "genre_id"=>"1", "notes"=>"", \
"authors_attributes"=>{"0"{"author_last"=>"McKinney", "author_first"=>"Jack"}}, \
"publisher"=>"", "pages"=>"", "language_id"=>"1", "location_id"=>"1"}}
So... the data's there. But how do I get to it to process it? When I post the data, here's that log:
Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE\
`authors`.`author_last` IS NULL LIMIT 1
I've done a fair bit of searching around to see what there is out there on nested attributes, and I see a lot on the form side, but not much on how to get to the assets once they're submitted.
Any explanation of why the solution which works does actually work would also be appreciated. Thanks.
First, this line
#author = #book.authors
assigns an array of authors to #author, but in your before_save you seem to be expecting a single author model. Secondly, the #author variable is scoped to the controller and will be empty in your model's scope.
Remove that line and try this in your Book class (not tested):
def determine_distinct_author
authors.each do |author|
Author.find_or_create_by_author_last( author )
end
end
1) The nested attributes is an array, and should be accessed as an array. Getting the submission from the one model to the next still presents quite a problem.
2) accepts_nested_attributes_for is probably not going to help out a whole lot, and there will undoubtedly be a lot of custom processing which will have to happen between now and when the whole system is fully functional. Probably best to ditch it now and move on.
In that direction: It looks like Ryan Bates has done a recent bit on the jQuery TokenInput plugin which will take care of a lot of the autocomplete feature on the author. More to the larger problem (and a bit harder to find), is a follow up that he posted to some issues with the plugin on working with new entries.
I am not quite sure what you are trying to accomplish, but inside your Author you have a before_save callback, and that is obviously giving the error.
To me it seems you are looking for an author with the given name, and then want to use that? Is that correct?
Using a nested form, you will always create a new author, unless you want to use something like a select-box, and then select an author-id (which is part of the book, so not nested), instead of the authors details.
UX-wise, i would offer the option to either select an existing author (use an autocomplete field), or create a new one (using the nested fields option --without your before_save callback).

Resources