Rails save copy of original when model is edited - ruby-on-rails

I have a simple scaffold:
rails g scaffold Order name:string notes:text
I would like for the user to be able to edit their order but keep copies of the past versions for reference. I considered adding a separate "Revision" model that would hold the text instead, but I would also like the form to stay populated with the most recent information for easy editing.
sorry for not posting more code, I'm not sure what would be helpful

I think that the better idea is create two models: Order and OrderHistory
Order will have a relation 1 to much to OrderHistory.
In update action of your OrderController you have to create a OrderHistory with the previous data and with foreign key pointing to Order. Also you can use default rails fields like create_date for getting the last revised version and mantain a good history of them.
Is a bad practise use Model Order for do that as I said.
class OrderController < ApplicationController::Base
before_action :set_order #implement this function
def update
orderHistory = OrderHistory.create(#order.params) #this code can fail I don't remmember how exactly do that now
respond_to do |format|
if #order.update(order_params)
orderHistory.save
else
#response in fail case
end
end
end
end

Related

Rails how to use 2 of the same slugs

I am using slugs in my project to give my params an other name but I have two params called: "how-does-it-work".
(.../investor/how-does-it-work)
(.../customer/how-does-it-work)
I would like to use the slugs as how they are currently set.
Is there a way to do that?
Create two distinct routes/controllers, and simply query the corresponding ActiveRecord model in the show action. Assuming there is a slug field on your models:
Rails.application.routes.draw do
resources :customers
resources :investors
end
class CustomersController < ApplicationController
def show
#customer = Customer.find_by(slug: params[:id])
end
end
class InvestorsController < ApplicationController
def show
#investor= Investor.find_by(slug: params[:id])
end
end
This is probably the most conventional way to solve this problem in Rails. If you are using the friendly_id gem, the same approach more or less applies, except for maybe the query itself.
Hope this helps.
So, is /investor/ and /customer/ both parts of the slug?
If that's the case, you can split the string, and do a search based on the "how-does-it-work" in the grouping of "investor" or "customer".
If investor and customer are both parts of the routes, you shouldn't have a difficult time there, because they're pointing to two different controller methods. You should be able to write a search based on each of those methods that correspond to the data. If the data is the same, all your doing is pointing the controller to the correct model data with the correct params.
If you're using friendlyId, it usually has built in candidate matching. Also, if you're meaning to match multiple pages to the same slug (which I've done in the past), you can display a results page if you'd like too, by rendering based on the quantity of results.

Rails preview update associations without saving to database

I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs

how to run a one-time database change on a single user

I have Customer and each customer has_many Properties. Customers belong to a Company.
I'm trying to add a certain Property to each one of a single Company's Customers. I only want this change to happen once.
I'm thinking about using a migration but it doesn't seem right to create a migration for a change that I only ever want to happen once, and only on one of my users.
Is there a right way to do this?
You can just use rails console.
In rails c:
Company.where(conditions).last.customers.each do |customer|
customer.properties << Property.where(condition)
customer.save!
end
Validation
Depending on how you're changing the Customer model, I'd include a simple vaidation on the before_update callback to see if the attribute is populated or not:
#app/models/Customer.rb
class Customer < ActiveRecord::Base
before_update :is_valid?
private
def is_valid?
return if self.attribute.present?
end
end
This will basically check if the model has the attribute populated. If it does, it means you'll then be able to update it, else it will break
--
Strong_Params
An alternative will be to set the strong_params so that the attribute you want to remain constant will not be changed when you update / create the element:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...
private
def strong_params
params.require(:model).permit(:only, :attributes, :to, :update)
end
end
It would be much more helpful if you explained the context as to why you need this type of functionality - that will give people the ability to create a real solution, instead of proposing ideas

Rails forms - Should I build `accepts_nested_attributes_for` associations in the Controller, Model, or View?

The Question
I have a parent that accepts_nested_attributes_for a child. So, when I have a form for the parent, I need to build the child so I can display form fields for it as well. What I want to know is: where should I build the child? In the Model, View, or Controller?
Why I Am Asking This
You may be shaking your head and thinking I'm a madman for asking a question like this, but here's the line of thinking that got me here.
I have a Customer model that accepts_nested_attributes_for a billing_address, like so:
class Customer
belongs_to :billing_address, class_name: 'Address'
accepts_nested_attributes_for :billing_address
end
When I present a form for a new Customer to the user, I want to make sure there is a blank billing_address, so that the user actually sees fields for the billing_address. So I have something like this in my controller:
def new
#customer = Customer.new
#customer.build_billing_address
end
However, if the user doesn't fill out any of the billing_address fields, but tries to submit an invalid form, they will be presented with a form that no longer has fields for the billing_address, unless I put something like this in the create action of my controller:
def create
#customer = Customer.new(params[:customer])
#customer.build_billing_address if #customer.billing_address.nil?
end
There is another issue, which is that if a user tries to edit a Customer, but that Customer doesn't have an associated billing_address already, they won't see fields for the billing_address. So I have to add somethign like this to the controller:
def edit
#customer = Customer.find(params[:id])
#customer.build_billing_address if #customer.billing_address.nil?
end
And something similar needs to happen in the controller's update method.
Anyway, this is highly repetitive, so I thought about doing something in the model. My initial thinking was to add a callback to the model's after_initialize event, like so:
class CustomerModel
after_initialize :build_billing_address, if: 'billing_address.nil?'
end
But my spidey sense started tingling. Who's to say I won't instantiate a Customer in some other part of my code in the future and have this wreak havoc in some unexpected ways.
So my current thinking is that the best place to do this is in the form view itself, since what I'm trying to accomplish is to have a blank billing_address for the form and the form itself is the only place in the code where I know for sure that I'm about to show a form for the billing_address.
But, you know, I'm just some guy on the Internet. Where should I build_billing_address?
Though this advice by Xavier Shay is from 2011, he suggests putting it in the view, "since this is a view problem (do we display fields or not?)":
app/helpers/form_helper.rb:
module FormHelper
def setup_user(user)
user.address ||= Address.new
user
end
end
app/views/users/_form.html.erb:
<%= form_for setup_user(#user) do |f| %>
Note that I had to change the helper method to the following:
def setup_user(user)
user.addresses.build if user.addresses.empty?
user
end
The controller remains completely unchanged.
If you know your model should always have a billing address, you can override the getter for this attribute in your model class as described in the docs:
def billing_address
super || build_billing_address
end
Optionally pass in any attributes to build_billing_address as required by your particular needs.
You would use build if you want to build up something and save it later. I would say, use it in nested routes.
def create
#address = #customer.billing_addresses.build(params[:billing_address])
if #address.save
redirect_to #customer.billing_addresses
else
render "create"
end
end
Something like that. I also use the build when I'm in the console.
You have to remember the principles of MVC, which is to create DRY(don't repeat yourself) code, which is efficiently distributed between the various moving parts of the app
accepts_nested_attributes_for Is Great For Keeping Things DRY
accepts_nested_attributes_for is a model function which allows you to pass data through an association to another model. The reason why it exists is to give you the ability to populate another model's data based on a single form, and is excellent for extending functionality without too much extra code
The problem you're citing is that if you want to use the code in other areas of the app, you'll end up having all sorts of problems
My rebuttal to that is in order to create as efficient an application as possible, you want to write as little code as possible - letting Rails handle everything. The accepts_nested_attributes_for function does allow you to do this, but obviously has a cost, in that you have to accommodate it every time you want to use it
My recommendation is to use what you feel is the most efficient code you can, but also keep to conventions; as this will ensure speed & efficiency
You should handle all these scenarios in controller, since it is not a responsibility of model.
Just in terms of keeping things DRY, you can write a method,
def build_customer(customer)
customer.build_billing_address if customer.billing_address.nil?
#add more code if needed
end
And inside controller you can call this method wherever it is needed. e.g.
def create
#customer = Customer.new(params[:customer])
if #customer.save
redirect_to #customer.billing_addresses
else
build_customer(#customer)
render "new"
end
end

has_many through blowing away the association's metadata on mass association

Hey,
Not a Rails noob but this has stumped me.
With has many through associations in Rails. When I mass assign wines to a winebar through a winelist association (or through) table with something like this.
class WineBarController
def update
#winebar = WineBar.find(params[:id])
#winebar.wines = Wine.find(params[:wine_bar][:wine_ids].split(",")) // Mass assign wines.
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
This will delete every winelist row associated with that winebar. Then it finds all of the wines in wine_ids, which we presume is a comma separated string of wine ids. Then it inserts back into the winelist a new association. This would be expensive, but fine if the destroyed association rows didn't have metadata such as the individual wine bar's price per glass and bottle.
Is there a way to have it not blow everything away, just do an enumerable comparison of the arrays and insert delete whatever changes. I feel like that's something rails does and I'm just missing something obvious.
Thanks.
Your problem looks like it's with your first statement in the update method - you're creating a new wine bar record, instead of loading an existing record and updating it. That's why when you examine the record, there's nothing showing of the relationship. Rails is smart enough not to drop/create every record on the list, so don't worry about that.
If you're using the standard rails setup for your forms:
<% form_for #wine_bar do |f| %>
Then you can call your update like this:
class WineBarController
def update
#winebar = WineBar.find(params[:id])
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
You don't need to explicitly update your record with params[:wine_bar][:wine_ids], because when you updated it with params[:wine_bar], the wine_ids were included as part of that. I hope this helps!
UPDATE: You mentioned that this doesn't work because of how the forms are setup, but you can fix it easily. In your form, you'll want to rename the input field from wine_bar[wine_ids] to wine_bar[wine_ids_string]. Then you just need to create the accessors in your model, like so:
class WineBar < ActiveRecord::Base
def wine_ids_string
wines.map(&:id).join(',')
end
def wine_ids_string= id_string
self.wine_ids = id_string.split(/,/)
end
end
The first method above is the "getter" - it takes the list of associated wine ids and converts them to a string that the form can use. The next method is the "setter", and it accepts a comma-delimited string of ids, and breaks it up into the array that wine_ids= accepts.
You might also be interested in my article Dynamic Form Elements in Rails, which outlines how rails form inputs aren't limited to the attributes in the database record. Any pair of accessor methods can be used.

Resources