I have a form in which I input a transfer. This transfer is stored into a database (and used for a transaction in the controller).
It worked roughly, but now for the finetuning the input needs modification before I save or a transaction is made.
The name of the recipient needs to be a transformed into this users id.
The amount has a two decimal number input and should be modified to cents by multiplying it by a hundred and making it an integer. Banks need a solid integer basis as you know.
I thought this could work:
Transfer model
#transfer.rb
has_one :sender,
:class_name => "User",
:foreign_key => "sender_id"
has_one :recipient,
:class_name => "User",
:foreign_key => "recipient_id"
validates :sender_id, presence: true
validates :recipient_id, presence: true
validates :amount, numericality: {:greater_than => 0}
before_validation :recipient_name_to_id
before_validation :amount_to_cents
def recipient_name_to_id
recipient = User.find_by_user_name(self.recipient_id)
self.recipient_id = recipient.id
end
def amount_to_cents
cents = self.amount
cents = cents*100
cents.to_i
self.amount = cents
end
To modify the :recipient_id and the :amount taken from the form while the app asks for a name and any number.
It doesn't work, because in the transaction it can't find the recipients banksaldo.
NoMethodError in TransfersController#create
undefined method `cents' for nil:NilClass
#account_b.cents += #transfer.amount
Everything else works fine. I checked it thoroughly.
But maybe it helps to add the controller
TransfersController
def create
#user = User.find(params[:user_id])
#transfer
#transfer = Transfer.new(transfer_params)
#transfer.sender_id = #user.id
#transactie tussen accounts
#account_a = #user.account
#account_b = Account.find_by_user_id(#transfer.recipient_id)
if #account_a != #account_b
if #account_a.cents >= #transfer.amount
Account.transaction do
#account_a.cents -= #transfer.amount
#account_a.save!
#account_b.cents += #transfer.amount
#account_b.save!
#transfer.save!
flash.notice = "Uw opdracht is verzonden."
end
else
flash.notice = "U beschikt niet over voldoende saldo om uw opdracht te doen slagen."
end
else
flash.notice = "U kunt geen geld overmaken naar uw eigen account."
end
redirect_to user_path(#user)
end
private
def transfer_params
params.require(:transfer).permit(:sender_id, :recipient_id, :amount)
end
Update
I am now working with current setting (is this more clever, or does it not really matter?).
#same transfer.rb model but now with this as non private methods.
def recipient_id=(value)
user = User.find_by_user_name(value)
self[:recipient_id] = user.id
end
def amount=(value)
cents = value * 100
cents.to_i
self[:amount] = cents
end
The code now gives:
Validation failed: Amount is not a number
and can't #transfer.save! in the controller.
Update 2
All right it nearly works. I had to make the value into a float first by to_f. Then it worked... until I turned on the validation again (which I commented out) and now it gives an error that amount is not an integer... Which I state in the def. Hmprff :S
Update 3
It all works now. My methods look like this now:
def recipient_id=(value)
user = User.find_by_user_name(value)
self[:recipient_id] = user.id
end
def amount=(value)
cents = value.to_f * 100
self[:amount] = cents.to_i
end
Bad thing about it is that with wrong form information the app gives it's validation error screen, while I want it to give a notice and redirect it to the show window where it was.
It can't find both methods flash and redirect in these methods I defined above in the transfermodel.
#account_b did not load, either because transfer_params[:recipient_id] is nil or because no Account exists with that ID. You might consider switching to Account.where(user_id: #transfer.recipient_id).first! so that it raises an exception, or you need to check whether #account_b.nil? and handle the problem. Because #account_a != nil, your logic continues through until you can't access #account_b.cents.
Check your form and incoming parameters to make sure params[:transfer][:recipient_id] is present, and then verify that user ID has an Account.
Related
I'm building an events app using Rails and have added a counter_cache association between my Event and Booking models in order to capture the number of bookings made on each event.
How do I get the counter_cache to update correctly when more than one booking can be made at any one time? At the moment, each time a booking is made, regardless of how many spaces for an event are required (3,4,5 etc) , the counter_cache will only increment by one. This creates an inaccuracy in number of bookings Vs number of spaces available (and total_amount ).
Is it as simple as -
def my_booking
event.bookings_count * booking.quantity
end
If so, where / how do I call this in my MVC process?
This is my Booking Model with set_booking method (for payments), but should a method be in here or in my Event model ?
class Booking < ActiveRecord::Base
belongs_to :event, counter_cache: true
belongs_to :user
before_create :set_booking_number
validates :quantity, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :total_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
validate(:validate_booking)
#validate(:validate_availability)
def set_booking_number
self.booking_number = "MAMA" + '- ' + SecureRandom.hex(4).upcase
end
def set_booking
if self.event.is_free?
self.total_amount = 0
save!
else
self.total_amount = event.price_pennies * self.quantity
begin
charge = Stripe::Charge.create(
amount: total_amount,
currency: "gbp",
source: stripe_token,
description: "Booking created for amount #{total_amount}")
self.stripe_charge_id = charge.id
save!
rescue Stripe::CardError => e
# if this fails stripe_charge_id will be null, but in case of update we just set it to nil again
self.stripe_charge_id = nil
# we check in validatition if nil
end
end
end
def validate_booking
# stripe_charge_id must be set for not free events
unless self.event.is_free?
return !self.stripe_charge_id.nil?
end
end
end
I think the best way is to implement your own caching method.
Class Event
def update_bookings_count(amount)
update_column(:bookings_count, "bookings_count + #{amount}")
end
end
After the booking was successful call:
event.update_bookings_count(quantity)
And just use event.bookings_count when you need the count.
Another option might be to look into this gem: https://github.com/magnusvk/counter_culture#totaling-instead-of-counting it seems to do what you want.
I have a rails question that I have been unable to find an answer for on my own. I apologize if it's very simple or obvious, I'm a complete beginner here.
So I have a column in my db called :client_code, which is defined in the model as the down-cased concatenation of the first character of :first_name, and :last_name. So for instance, if I pull up a new form and enter 'John' for :first_name, and 'Doe' for :last_name, :client_code is automatically given the value of 'jdoe'. Here's the relevant portion of the model code:
class Client < ActiveRecord::Base
before_validation :client_code_default_format
validates_presence_of :first_name, :last_name, :email
validates_uniqueness_of :client_code
...
def client_code_default_format
self.client_code = "#{first_name[0]}#{last_name}".downcase
end
end
I would like to add something to this code so that, in the event that someone enters a different client with the same exact name, it does't fail the uniqueness validation but instead creates a slightly modified :client_code ('jdoe2', for example). I could probably figure out how to add an index to all of them, but I would prefer to only include numbers as a failsafe in the case of duplicates. Can anyone point me in the right direction?
Calculating the number of current matching Client objects with the same client_code should work
def client_code_default_format
preferred_client_code = "#{first_name[0]}#{last_name}".downcase
count = Client.count(:conditions => "client_code = #{preferred_client_code}")
self.client_code = count == 0 ? preferred_client_code : preferred_client_code + count
end
Many thanks to #Dieseltime for his response. I took his suggestion and was able to get the functionality I wanted with some minor changes:
before_validation :format_client_code
validates_presence_of :first_name, :last_name, :email, :company_id
validates_uniqueness_of :client_code
...
def format_client_code
unless self.client_code != nil
default_client_code = "#{first_name[0]}#{last_name}".downcase
count = Client.count(:conditions => "client_code = '#{default_client_code}'")
if count == 0
self.client_code = default_client_code
else
self.client_code = default_client_code + (count + 1).to_s
end
end
end
I'm building an application where users can purchase tracking numbers. I have an Order model and an Order Transaction model. If the Order Transaction returns from the gateway with success, I'm using an after_save callback to trigger a method that creates the tracking numbers and inserts them into the database. Sometimes a user just orders one, but if they order more than one, I can't seem to get rails to create and insert more than one record.
Here's what I'm using -- I've never had to user a loop like this, so I'm not sure what I'm doing wrong.
def create_trackables
if self.success == true
#order = Order.find(order_id)
#start = 0
while #start < #order.total_tokens
#trackable_token = Tracker.create_trackable_token
#start += 1
#trackable ||= Tracker.new(
:user_id => #current_user,
:token => #trackable_token,
:order_id => order_id
)
#trackable.save
end
end
end
dmarkow is right that you should use trackable instead of #trackable but you also should be using = instead of ||=. You also might as well just use create. Here's how I'd write it: def create_trackables
return unless self.success
order = Order.find(order_id) #you shouldn't need this line if it has_one :order
1.upto(order.total_tokens) do
Tracker.create!(
:user_id => #current_user,
:token => Tracker.create_trackable_token,
:order_id => order_id
)
end
end
Change #trackable to trackable to keep it scoped to the loop. Otherwise, the second time the loop runs, #trackable already has a value so the call to Tracker.new doesn't execute, and the #trackable.save line just keeps re-saving the same record. (Edit: Also remove the ||= and just use =).
def create_trackables
if self.success == true
#order = Order.find(order_id)
#start = 0
while #start < #order.total_tokens
#trackable_token = Tracker.create_trackable_token
#start += 1
trackable = Tracker.new(
:user_id => #current_user,
:token => #trackable_token,
:order_id => order_id
)
trackable.save
end
end
end
I'm messing around with a test/exercise project just to understand Rails better.
In my case I have three models: Shop, User and Product.
A Shop can be of three types: basic, medium, large. Basic can have 10 Products maximum, medium 50, large 100.
I'm trying to validate this kind of data, the type of Shop and check how many products it owns when creating a new product.
So far, I came up with this code (in shop.rb) but it doesn't work:
def lol
account = Shop.find_by_sql "SELECT account FROM shops WHERE user_id = 4 LIMIT 1"
products = Product.count_by_sql "SELECT COUNT(*) FROM products WHERE shop_id = 13"
if account = 1 && products >= 10
raise "message"
elsif account = 2 && products >= 50
raise "message"
else account = 3 && products >= 100
raise "message"
end
end
I don't even know if the logic behind my solution is right or what. Maybe I should validate using
has_many
and its "size" method? I don't know. :)
At least change account = 1 to account == 1. Same goes for account = 2 and account = 3.
Other than that I would recommend you looking at Rails Guides to get a feel for using Rails.
That being said, I suggest something like this:
class Shop < ActiveRecord::Base
has_many :products
validates :products_within_limit
# Instead of the 'account' column, you could make a 'max_size' column.
# Then you can simply do:
def products_within_limit
if products.size > max_size
errors.add_to_base("Shop cannot own more products than its limit")
end
end
def is_basic?
products.size >= 10 && products.size < 50
end
def is_medium?
products.size >= 50 && products.size < 100
end
def is_big?
products.size >= 100
end
def shop_size
if self.is_basic?
'basic'
elsif self.is_medium?
'medium'
elsif self.is_big?
'big'
end
end
end
This allows you to do:
# Get shop with id = 1
shop = Shop.find(1)
# Suppose shop '1' has 18 products:
shop.is_big? # output false
shop.is_medium? # output false
shop.is_basic? # output true
shop.shop_size # output 'basic'
it does not need to be so hard:
class Shop < ActiveRecord::Base
has_many :products
validate :check_nr_of_products
def check_nr_of_products
nr_of_products = products.size
errors[:base] << "Basic shops can have max 10 products" if account == 1 && nr_of_products > 10
errors[:base] << "Medium shops can have max 50 products" if account == 2 && nr_of_products > 50
errors[:base] << "Big shops can have max 100 products" if account == 3 && nr_of_products > 100
end
this validation is checked every time you save. You do not need to retrieve the "account-type", assuming it is a field of the shop. Likewise, instead of writing the query to count the nr of products, use the size function that does just that.
This is a simple solution. The STI solution suggested by #Dave_Sims is valid and more object-oriented.
Here's one possible more Rails-y way of achieving this. This is my own flavor of making Ruby imitate the behavior of an abstract class (Shop). YMMV.
EDIT: Note that I'm replacing the 'account' variable from OP's example with inheritance using ActiveRecord's Single Table Inheritance, which uses a 'type' column to perform basically the same function, but using inheritance to express the different kinds of shops and their respective product limits. OP's original example likely violates the Liskov Substitution Principle, and STI is one way of fixing that.
EDIT: As if I wasn't being pedantic enough, technically it's not really a Liskov violation as much as an Open/Closed violation. They're all variations on the same theme. You get the idea.
class Product < ActiveRecord::Base
belongs_to :shop
end
class Shop < ActiveRecord::Base
has_many :products
belongs_to :user
validates :products_within_limit
def products_within_limit
if products.count > limit
errors.add_to_base("Shop cannot own more products than its limit")
end
end
def limit
raise "limit must be overridden by a subclass of Shop."
end
end
class BasicShop < Shop
def limit
10
end
end
class MediumShop < Shop
def limit
50
end
end
class LargeShop < Shop
def limit
100
end
end
shop = BasicShop.create
10.times {Product.create(:shop => shop)}
shop.reload
shop.valid? # is true
shop.products << Product.new
shop.valid? # is false
This should help you.
Well, first of all thanks to everyone for the big help and insightful discussion. :)
I took bits from your answers in order to assemble a solution that I can understand myself. It seems when it comes to programming I can only understand if else statements, nothing more complex. :(
What I did was:
class Shop < ActiveRecord::Base
belongs_to :user
has_many :products, :dependent => :destroy
validate :is_account
def is_account
if account == 1 && products.size < 11
elsif account == 2 && products.size < 51
else account == 3 && products.size < 101
end
end
Then in products_controller.rb I put these lines:
def new
if current_user.shop.nil?
flash[:notice] = I18n.t 'shops.create.must' #this should check is the user owns a shop, otherwise can't add a product. It seems to work, so far
redirect_to :action => :index
elsif current_user.shop.valid?
flash[:notice] = I18n.t 'shops.upgrade.must'
redirect_to :action => :index
else
#product = Product.new
end
end
The shop now is a type 1 and has only 9 products but whenever I click the "New Product" link I'm redirected to /products with the shops.upgrade.must message.
I don't know, it seems that
account
in shop.rb doesn't return the correct value. That column is a int(11) type, so I guess it could only return a number, but still...
Again, thanks for the huge support. I ended up stealing bits from your solutions and implementing this code:
#in shop.rb
validate :is_account
def is_account
if account == 1
limit = 10
elsif account == 2
limit = 50
else account == 3
limit = 100
end
errors.add(:base, "Reached maximum number of items for shop") if account == account && products.size >= limit
end
#in products_controller.rb
def new
if current_user.shop.nil?
flash[:alert] = I18n.t 'shops.create.must'
redirect_to :action => :index
elsif current_user.shop.invalid?
flash[:alert] = I18n.t 'shops.upgrade.must'
redirect_to :action => :index
else
#product = Product.new
end
end
It seems to work so far. Hope I didn't make any blatant mistake.
Thanks again! :)
I have a user model in which I have a method for seeing if the user has earned a "badge"
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1)
end
If they have earned a badge, the the award_badge method runs and gives the user the associated badge. Can I do something like this?
def check_if_badges_earned(user)
if user.recipes.count > 10
flash.now[:notice] = "you got a badge!"
award_badge(1)
end
Bonus Question! (lame, I know)
Where would the best place for me to keep all of these "conditions" for which my users could earn badges, similar to stackoverflows badges I suppose. I mean in terms of architecture, I already have badge and badgings models.
How can I organize the conditions in which they are earned? some of them are vary complex, like the user has logged in 100 times without commenting once. etc. so there doesn’t seem to be a simple place to put this sort of logic since it spans pretty much every model.
I'm sorry for you but the flash hash is not accessible in models, it gets created when the request is handled in your controller. You still can use implement your method storing the badge infos (flash message included) in a badge object that belongs to your users:
class Badge
# columns:
# t.string :name
# seed datas:
# Badge.create(:name => "Recipeador", :description => "Posted 10 recipes")
# Badge.create(:name => "Answering Machine", :description => "Answered 1k questions")
end
class User
#...
has_many :badges
def earn_badges
awards = []
awards << earn(Badge.find(:conditions => { :name => "Recipeador" })) if user.recipes.count > 10
awards << earn(Badge.find(:conditions => { :name => "Answering Machine" })) if user.answers.valids.count > 1000 # an example
# I would also change the finds with some id (constant) for speedup
awards
end
end
then:
class YourController
def your_action
#user = User.find(# the way you like)...
flash[:notice] = "You earned these badges: "+ #user.earn_badges.map(:&name).join(", ")
#...
end
end