I'm using single table inheritance successfully like so:
class Transaction < ActiveRecord::Base
belongs_to :order
end
class Purchase < Transaction
end
class Refund < Transaction
end
The abbreviated/simplified PurchaseController looks like this:
class PurchaseController < TransactionController
def new
#transaction = #order.purchases.new(type: type)
end
def create
#transaction = #order.purchases.new secure_params
if #transaction.save
redirect_to #order
else
render :new
end
end
end
The abbreviated/simplified Purchase model looks like this:
class Purchase < Transaction
attr_accessor :cc_number, :cc_expiry, :cc_csv
end
What I'm trying to do is have different variations of a purchase, for instance a cash purchase & a cheque purchase. The issue is I'm not sure how to call the model for that variation.
For example:
class Cash < Purchase
attr_accessor :receipt_no
end
class CashController < TransactionController
def new
# This will use the Purchase.rb model so that's no good because I need the Cash.rb model attributes
#transaction = #order.purchases.new(type: type)
# This gives me the following error:
# ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Purchase is not a subclass of Cash
#transaction = Cash.new(type: 'Purchase', order: #order.id)
end
end
I'm not sure why it doesn't work for you, this works fine for me:
#order.purchases.new(type: "Cash") # returns a new Cash instance
You can also push a new Cash on to the association if you are ready to save it:
#order.purchases << Cash.new
Or you can define a separate association in Order:
class Order < ActiveRecord::Base
has_many :cashes
end
#order.cashes.new # returns a new Cash instance
Class
Maybe I'm being obtuse, but perhaps you'll be willing to not make the Purchase type an inherited class?
The problem I see is that you're calling Cash.new, when really you may be better to include all the functionality you require in the Purchase model, which will then be able to be re-factored afterwards.
Specifically, why don't you just include your own type attribute in your Purchase model, which you'll then be able to use with the following setup:
#app/controllers/cash_controller.rb
class CashController < ApplicationController
def new
#transaction = Purchase.new
end
def create
#transaction = Purchase.new transaction_params
#transaction.type ||= "cash"
end
private
def cash_params
params.require(:transaction).permit(:x, :y, :z)
end
end
The only downside to this would be that if you wanted to include different business logic for each type of purchase, you'll still want to use your inherited model. However, you could simply split the functionality in the before_create callback:
#app/models/puchase.rb
class Purchase < Transaction
before_create :set_type
private
def set_type
if type =="cash"
# do something here
else
# do something here
end
end
end
As such, right now, I think your use of two separate models (Cash and Cheque) will likely be causing much more of an issue than is present. Although I'd love to see how you could inherit from an inherited Model, what I've provided is something you also may wish to look into
Related
I have the following models:
class Page < ApplicationRecord
has_one :architecture
end
class Architecture < ApplicationRecord
belongs_to :page
end
And after a new page is saved I need to capture it's architecture (number of paragraphs por example). I would like to know what is the proper way to do that. I'm not sure if I should leave that responsible for the Page model:
class Page < ApplicationRecord
has_one :architecture
after_create :scrape_architecture
private
def scrape_architecture
data = call_something_to_capture_architecture(url)
create_architecture(data)
end
end
class Architecture < ApplicationRecord
belongs_to :page
end
or if it should be the responsibility of the Architecture model:
class Page < ApplicationRecord
has_one :architecture
after_create :create_architecture
end
class Architecture < ApplicationRecord
belongs_to :page
before_create :scrape_page
private
def scrape_page
data = call_something_to_capture_architecture(page.url)
create(data)
end
end
Which is actually incorrectly because before_create runs after the validation – causing MySQL errors duo to non null constraints
Thank you.
I would just create a job or service object that handles scraping.
class PageScrapingJob < ApplicationJob
queue_as :default
def perform(page)
data = call_something_to_capture_architecture(page.url)
architecture = page.create_actitecture(data)
# ...
end
end
You would then call the service/job in your controller after saving the page:
class PagesController < ApplicationController
def create
#page = Page.new(page_params)
if #page.save
PageScrapingJob.perform_now(#page)
redirect_to #page
else
render :new
end
end
end
This gives you a perfect control of exactly when this is fired and avoids placing even more responsibilities onto your models. Even though your models may contain little code they have a huge amount of responsibilities such as validations, I18n, form binding, dirty tracking etc that are provided by ActiveModel and ActiveRecord. The list really goes on and on.
This instead creates a discrete object that does only one job (and hopefully does it well) and that can be tested in isolation from the controller.
For such things you could use a service pattern
class PageScrapper
Result = Struct.new(:success?, :data)
def initialize(url)
#url = url
end
def call
result = process(#url)
...
if result.success? # pseudo methods
Result.new(true, result)
else
Result.new(false, nil)
end
end
end
class Fetcher
...
def call
scrapper = PageScrapper.new(url)
result = scrapper.call
if scrapper.success?
page = Page.build(parsed_result_if_needed(result)
page.architecture.build(what_you_need)
page.save # here you need to add error handling if save fails
else
# error handling
end
end
There are a lot of resources about why callbacks are bad.
Here is one from Marcin Grzywaczewski but you can also google it "why callbacks are bad ruby on rails".
By using service you are liberating models from having too much business logic and they do not need to know about other parts of your system.
I am newbie in rails and I am building a bank account application in rails. The user and account data is already available in seeds. User has only one account so the association here is of one-to-one. However one account can have multiple transactions. So the association is of one-to-many. So when user clicks on 'transact' option for his account,he is directed to transaction page for debit/credit transaction. But once the transaction is performed, the details of transaction along with account_id should be stored in a transaction table.
My code for accounts and transactions is as follows:
Model Account:
class Account < ApplicationRecord
belongs_to :user
has_many :transaction
end
Account Controller:
class AccountsController < ApplicationController
def index
#accounts = Account.all
end
end
Transaction Model:
class Transaction < ApplicationRecord
belongs_to :account
end
Transaction Controller:
class TransactionsController < ApplicationController
def new
#transaction = Transaction.new
end
def create
#transaction = Transaction.new(transaction_params)
end
private
def transaction_params
params.require(:transaction).permit(:amount, :commit)
end
end
I have added account_id column in transaction table. Can anybody please help to setup the association??
Thanks in advance.
I think you have transactions and for particular account, and you are proceeding transaction through account.
Scenario-1 :
Checked in particular account & proceed transaction. (create view)
Nested routes for transaction is created, ex. for new action, /accounts/:account_id/transactions/new
create filter, before_filter :find_account for
#account = Account.find(params[:account_id]) if params[:account_id]
In new action of TransactionsController,
#transaction = #account.transactions.new
You need to update transaction_params in case of changes
Scenorio-2 :
If you are not creating nested routing, simply provide #account_list_hash where you will select account_id by using select_list in view. And new action will be
# code to pass array for account_id select_list by method account_list_hash
#transaction = #Transaction.new
I will update in case of further clarification.
In Account model change this:
has_many: transaction to has_many: transactions
Please follow the naming conventions.
And in TransactionsController create a before_action like below.
before_action :set_account_id
def set_account_id
#account = Account.find_by(id: params[:account_id]
end
While creating a transaction create using the Account details returned by set_account_id method.
class TransactionsController < ApplicationController
def create
#transaction = #account.transactions.new(transaction_params)
#transaction.save!
end
end
I have Rails 4 app with two models.
class User
has_many :bids
end
class Bid
belongs_to :user
end
A User can only create one bid per week, so I added the following to the Bid table
add_column :bids, :expiry, :datetime, default: DateTime.current.end_of_week
and the following scopes to the Bid model
scope :default, -> { order('bids.created_at DESC') }
scope :active, -> { default.where('expiry > ?', Date.today ) }
I can now prevent a User creating multiple Bids at the controller level like so:
class BidsController
def new
if current_user.bids.active.any?
flash[:notice] = "You already have an active Bid. You can edit it here."
redirect_to edit_bid_path(current_user.bids.active.last)
else
#bid = Bid.new
respond_with(#bid)
end
end
end
But what is the best approach for validating this at the model level?
I've been trying to set up a custom validation, but am struggling to see the best way to set this up so that the current_user is available to the method. Also, am I adding errors to the correct object?
class Bid
validate :validates_number_of_active_bids
def validates_number_of_active_bids
if Bid.active.where(user_id: current_user).any?
errors.add(:bid, "too much")
end
end
end
In order to maintain separation of concerns, keep the current_user knowledge out of the model layer. Your Bid model already has a user_id attribute. Also, I'd add an error like so since the validation is not checking a "bid" attribute on Bid, but rather the entire Bid may be invalid.
class Bid
validate :validates_number_of_active_bids
def validates_number_of_active_bids
if Bid.where(user_id: user_id).active.any?
errors[:base] << "A new bid cannot be created until the current one expires"
end
end
end
This seems like it should be in a collaborator service object. Create a new class that is named appropriately (something like ActiveBid, maybe think a little on the name) That class will be initialized with a current_user and either return the active bid or false.
This limits the logic for this limitation into a single place (maybe some plans in the future can have 2, etc.
Then in the controller do a before_action that enforces this logic.
before_action :enforce_bid_limits, only: [:new, create]
private
def enforce_bid_limits
active_bid = ActiveBid.new(current_user).call
if active_bid #returns false or the record of the current_bid
flash[:notice] = "You already have an active Bid. You can edit it here."
redirect_to edit_bid_path(bid)
end
end
Later on if you end up needing this logic in several places throw this stuff in a module and then you can just include it in the controllers that you want.
I'm trying to associate a shipping address with an order in my application but cannot figure out a good way to do this.
This is the association I have:
class ShippingAddress < ActiveRecord::Base
belongs_to :order
end
and
class Order < ActiveRecord::Base
has_one :shipping_address
end
I see a lot of examples assuming there is a current_user, but I am trying to make the checkout process so that a user does not have to be signed in.
Here is what my Shipping Addresses controller looks like:
class ShippingAddressesController < ApplicationController
def new
#shipping_address = ShippingAddress.new
end
def create
#shipping_address = ShippingAddress.new(shipping_address_params)
#shipping_address.save
redirect_to root_url
end
private
def shipping_address_params
params.require(:shipping_address).permit(:shipping_name, :address_line1, :address_line2, :address_city,
:address_state, :address_zip, :address_country, :user_id, :order_id)
end
end
This is what the order controller looks like:
class OrdersController < ApplicationController
def create
#order = Order.find_or_initialize_by(item_id: params[:order][:item_id], order_completed: false, user_id: params[:order][:user_id])
#order.update_attribute(:quantity_requested, params[:order][:quantity_requested])
#order.save
redirect_to #order
end
private
def order_params
params.require(:order).permit(:user_id, :item_id, :quantity_requested, :quantity,
:order_completed, :sold, :shipping_address_id)
end
end
Can someone please let me know if I'm thinking about this the right way and how I can make the order aware of the shipping address?
Thanks in advance and sorry if I'm missing something obvious.
Not sure if you really need a :user_id in parameters. If user is logged in, you can get it via current_user. If not, you'll never know his id anyway.
Order is aware of the shipping address when you create it like order.create_shipping_address or ShippingAddress.create order: order (or populate shipping_addresses.order_id field any other way you like). And then you can use order.shipping_address and shipping_address.order to find related objects.
Also, usually managers(or site) have to communicate with customer anyway, at least send some emails regarding order status, so I don't require user to log in, but if he is not logged in, I create a new one on checkout to store his contacts(and for some other useful stuff :) )
I would like to use an after_save callback to set the updated_by column to the current_user. But the current_user isn't available in the model. How should I do this?
You need to handle it in the controller. First execute the save on the model, then if successful update the record field.
Example
class MyController < ActionController::Base
def index
if record.save
record.update_attribute :updated_by, current_user.id
end
end
end
Another alternative (I prefer this one) is to create a custom method in your model that wraps the logic. For example
class Record < ActiveRecord::Base
def save_by(user)
self.updated_by = user.id
self.save
end
end
class MyController < ActionController::Base
def index
...
record.save_by(current_user)
end
end
I have implemented this monkeypatch based on Simone Carletti's advice, as far as I could tell touch only does timestamps, not the users id. Is there anything wrong with this? This is designed to work with a devise current_user.
class ActiveRecord::Base
def save_with_user(user)
self.updated_by_user = user unless user.blank?
save
end
def update_attributes_with_user(attributes, user)
self.updated_by_user = user unless user.blank?
update_attributes(attributes)
end
end
And then the create and update methods call these like so:
#foo.save_with_user(current_user)
#foo.update_attributes_with_user(params[:foo], current_user)