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
Related
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.
In a RSpec spec file I have the following test
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.find(:all,
:select => ["*,(abs(rating - current_user.rating)) as player_rating"],
:order => "player_rating",
:limit => 5)
# test that matched_players array returns what it is suppose to
end
How would I complete this to test that matched_players is returning the correct users.
I think you should first introduce some test users to the test DB (using for example a Factory) and afterwards see that the test is returning the correct ones.
Also it would make more sense to have a method in your model that would return the matched users.
For example:
describe "Player matching" do
before(:each) do
#user1 = FactoryGirl.create(:user, :rating => 5)
...
#user7 = FactoryGirl.create(:user, :rating => 3)
end
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.matched_players
matched_players.should eql [#user1,#user3,#user4,#user5,#user6]
end
end
Your model shouldn't know about your current user (the controllers know about this concept)
You need to extract this as a method on the User class otherwise there's no point in testing it, i.e. why test logic that isn't even in your app code?
The function that gets the matched players doesn't need to know about the current user, or any user for that matter, just the rating.
To test it, create a bunch of User instances, call the method, and see that the result is a list of the correct user instances you expect.
models/user.rb
class User < ActiveRecord::Base
...
def self.matched_players(current_user_rating)
find(:all,
select: ["*,(abs(rating - #{current_user_rating)) as match_strength"],
order: "match_strength",
limit: 5)
end
...
end
spec/models/user_spec.rb
describe User do
...
describe "::matched_players" do
context "when there are at least 5 users" do
before do
10.times.each do |n|
instance_variable_set "#user#{n}", User.create(rating: n)
end
end
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4.2)
matched_players.should == [#user4, #user5, #user3, #user6, #user2]
end
context "when multiple players have ratings close to the given rating and are equidistant" do
# we don't care how 'ties' are broken
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4)
matched_players[0].should == #user4
matched_players[1,2].should =~ [#user5, #user3]
matched_players[3,4].should =~ [#user6, #user2]
end
end
end
context "when there are fewer than 5 players in total" do
...
end
...
end
...
end
I have a Rails app with an Order and a Refund model. Order has_many :refunds. All well and good. I'm trying to write a functional test for refund logic in a controller. Here's what I have right now:
test "should not process partial paypal refund for partially refunded order when refund total plus refund amount is greater than order total" do
set_super_admin_login_credentials
o = Order.new
o.stubs({:id => 1234567, :source => "PayPal", :total => 39.95, :user => users(:dave)})
Order.stubs(:find).with(1234567).returns(o)
get :refund, {:order_id => 1234567}
assert_equal o, assigns(:order)
o.refunds.build.stubs({:amount => 1.0})
o.refunds.build.stubs({:amount => 30.00})
assert_raise do
post :refund, {:order_id => 1234567, :refund_amount => 10.00}
end
end
And in the controller, the refund method looks like this:
def refund
#order = Order.find(params[:order_id])
return if request.get?
amount = params[:refund_amount].to_f
raise "Cannot refund order for more than total" if (#order.refunds.sum(&:amount) + amount)
# Do refund stuff
end
Some notes:
I'm basing the o.refunds.build bit on Ryan Bates' Railscast. If this is not right or no longer relevant, that's helpful information.
I've seen a lot of conflicting information about how to actually do the sum method, some with the & and some without. In script/console, the & blows up but without it, I get an actual sum. In my controller, however, if I switch from &:amount to :amount, I get this message: NoMethodError: undefined method+' for :amount:Symbol`
I feel like there's some conceptual information missing rather than a bug somewhere, so I'll appreciate some pointers.
Finally figured out the issue. I was stubbing an empty association as [] rather than leaving it nil for Rails to handle on some other methods. So, when I would change one, the other would fail. Word to the wise: Enumerable#sum and ActiveRecord::Associations::AssociationCollection#sum take entirely different parameters. :)
So, by changing the stubs to leave off :refunds => [] and using a string for the field name in sum I got things back to normal. So, here's the functional version of the above code:
test "should not process partial paypal refund for partially refunded order when refund total plus refund amount is greater than order total" do
set_super_admin_login_credentials
o = Order.new
o.stubs({:id => 1234567, :source => "PayPal", :total => 39.95, :user => users(:dave)})
Order.stubs(:find).with(1234567).returns(o)
get :refund, {:order_id => 1234567}
assert_equal o, assigns(:order)
o.refunds.build.stubs({:amount => 1.0})
o.refunds.build.stubs({:amount => 30.00})
assert_raise do
post :refund, {:order_id => 1234567, :refund_amount => 10.00}
end
end
def refund
#order = Order.find(params[:order_id])
return if request.get?
amount = params[:refund_amount].to_f
raise "Cannot refund order for more than total" if (#order.refunds.sum('amount') + amount)
# Do refund stuff
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 am using Mongoid and have a project and a user model.
in the Project model, I have a field
class Project
include Mongoid::Document
field :name
field :user_ids, :type => Array
end
class User
include Mongoid::Document
field :email
end
I can find all the users belonging to one project, i.e., 'find this project's users'
#project = Project.first # => 'Housework'
User.criteria.id(#project.user_ids) # => ['Bart','Lisa','Maggie']
But I am having a bit trouble finding all the projects belonging to one user, i.e, 'find this user's projects'
#user = User.first # => 'Bart'
Project.where(:user_ids => #user.id) # doesn't work
Project.where(:user_ids.includes => #user.id) # not such method
Project.where(:user_ids => [#user.id]) # doesn't make sense to compare arrays, but tried anyway and doesn't work
I know that you can have another field in the User model to store project_ids, I would gladly do that, but I am just curious, is there a method to be used in finder conditions that works similarly to #includes? in ruby?
I found a solution to this. it is the all_in finder method
example:
Fruit.all[0].colors = ['red','green','blue'] #=> apple
Fruit.all[1].colors = ['yellow','green'] #=> banana
Fruit.all[2].colors = ['red', 'yellow'] #=> pineapple
To find all fruits that have the color red in their 'colors' array field, one can query:
Fruit.all_in(:colors => ['red'])
=>[apple, pineapple]