I am looking for a way of designing a Balance model between two users in Ruby on Rails (but could be more general). Two users Alice and Bob would start with a balance of value zero with regards to the other one. Now, if Alice gets a +4, I want Bob to have -4. Here's what I came up with so far:
A balance with three fields : user_one, user_two, and one_to_two. That is if I want the balance for user_one to user_two I just take one_to_two, and if I want from two_to_one I take -one_to_two (just a method in the balance model). The problem is that I could not simply use 'has_many balances through' from the User model, since the balance would need to know if it's user_one or user_two who is calling (I would have to pass the User)
A balance with three fields again, but completely asymmetric: if I create a balance for Alice to Bob, I would have to create another instance of balance from Bob to Alice. This would double the storage needed for each 'balance' in the database, and would require updating the bob_to_alice everytime I update alice_to_bob.
Can anyone come up with something better, or explain the reason for choosing one of the above? Let me know if I can make anything clearer.
So, I think I found something pretty neat. It uses the fact that user alice is always the one with the smallest id (via :ensure_alice_before_bob). The rest should be clear.
The User model just has two extra "has_many :balances_as_alice, has_many :balances_as_bob". It could work with only one (for instance :balances_as_alice), but with those two present + dependent: :destroy you make sure to avoid orphan balances.
# == Schema Information
#
# Table name: balances
#
# id :integer not null, primary key
# alice_id :integer not null
# bob_id :integer not null
# alice_value :float default("0.0")
# created_at :datetime not null
# updated_at :datetime not null
#
class Balance < ActiveRecord::Base
belongs_to :alice, class_name: 'User'
belongs_to :bob, class_name: 'User'
before_validation :ensure_alice_before_bob
validates :alice_id, presence: true
validates :bob_id, presence: true, uniqueness: { scope: :alice_id}
validate :different_users
def value
#for_bob ? -alice_value : alice_value
end
def value=(new_val)
#for_bob? self.alice_value = -new_val : self.alice_value = new_val
end
def Balance.get_for alice, bob
user = 'alice'
alice, bob, user = bob, alice, 'bob' if alice.id > bob.id
balance= alice.balances_as_alice.find_by(bob: bob) ||
Balance.new(bob: bob, alice: alice, alice_value: 0.0)
balance.send("for_#{user}")
end
def for_bob
#for_bob = true; self
end
def for_alice
#for_bob = false; self
end
private
def ensure_alice_before_bob
return if self.alice_id.nil? || self.bob_id.nil?
unless self.alice_id < self.bob_id
self.bob_id, self.alice_id = self.alice_id, self.bob_id
end
end
def different_users
if self.alice_id == self.bob_id
errors.add(:bob_id, "Cannot be the same user")
end
end
end
Related
I'm using an ActiveRecord::Base.transaction to make a whole bunch of calls pertaining to a grouping of objects that must update/create simultaneously or not at all. One method in this transaction is supposed to use where to find and delete all Trades that match certain parameters.
class Trade < ActiveRecord::Base
include Discard::Model
belongs_to :trade_requester, class_name: "User"
belongs_to :wanted_share, class_name: "Share"
validates :trade_requester, :wanted_share, presence: true
validates :amount, presence: true
def new_wanted_total
wanted_share.amount - amount
end
def update_wanted_share_amount(new_wanted_total)
wanted_share.update_attribute(:amount, new_wanted_total)
end
def delete_extraneous_wanted_trades(wanted_share)
self.class.where("wanted_share_id = ? AND amount > ? AND approved_at = ? AND discarded_at = ?", wanted_share.id, new_wanted_total, nil, nil).delete_all
end
def accept
ActiveRecord::Base.transaction do
delete_extraneous_wanted_trades(wanted_share)
update_wanted_share_amount(new_wanted_total) if new_wanted_total >= 0
Share.create(user_id: self.trade_requester.id, item_id: self.wanted_share.item.id, amount: self.amount, active: false)
self.touch(:approved_at)
end
end
end
When I accept and check the output in my terminal, one line I get says this:
SQL (0.3ms) DELETE FROM "trades" WHERE (wanted_share_id = 8 AND amount > 25 AND approved_at = NULL AND discarded_at = NULL).
I am passing the correct information to the method, and the rest of the terminal output shows that the related records have been updated with the appropriate attributes (one Share set to amount:25 and another Share created with amount:50). But then I check my database, and it says that there is still one Trade for amount: 60. This record exceeds the available total, which is now 50 (it was previously 75), and should have been deleted. But according to the terminal output, it was ignored. Why did this record go untouched?
approved_at = ? AND discarded_at = ? in the where clause should be approved_at IS NULL AND discarded_at IS NULL
There are two models:
# Table name: activities_people
#
# activity_id :integer not null
# person_id :integer not null
# date :date not null
# id :integer not null, primary key
# == Schema Information
#
# Table name: activities
#
# id :integer not null, primary key
# name :string(20) not null
# description :text
# active :boolean not null
# day_of_week :string(20) not null
# start_on :time not null
# end_on :time not null
Relations:
activity.rb
has_many :activities_people
has_many :people, through: :activities_people
activity_people.rb
belongs_to :person
belongs_to :activity
I try to create validation that person can join to one activity taking place in specific date and time(start_on, end_on). If I will try sign up to another activity while before I joined to other exercises(same date, and times overlap) should throw error.
What I try:
def check_join_client
activities_date = person.activities_people.where('date = date', date: date)
if activities_date.exists && person.activities.where('id IN (?)', activities_date)
end
I don't know how to use create query(person.activities.where ...) to getting person activities related with activies_people. activities_date check if we joined to activities taking place in same date. Second I want get check start_on and end_on.
Thanks in advance.
If I'm understanding you correctly, you want to find the activites_people for a user that match a query by the date array and then raise an error unless an associated activity for those matched activities_people.
Your original code for check_join_client uses if incorrectly:
def check_join_client
activities_date = person.activities_people.where('date = date', date: date)
if activities_date.exists && person.activities.where('id IN (?)', activities_date)
end
To translate this to pseudocode, you're essentially saying:
result = query if result.condition_met?
However the if condition (the expression after the if) will be evaluated before you define results. It might be more clear if I show a correct approach:
result = query
return result if result.condition_met?
Now to go back to your question about loading associated records, try something like this:
activities_people_ids_matching_date = person.activities_people
.where(date: self.date)
.pluck(:id)
# use pluck to get an array of ids because that's all that's needed here
# not sure how you're getting the date variable, so I'm assuming self.date will work
# I can use `.any?` to see if the ids list is not empty.
condition_met = activities_people_ids_matching_date.any? &&\
person.activities
.where(activities_people_id: activities_people_ids_matching_date)
.any?
condition_met ? true : raise(StandardError, "my error")
There surely is a way to get this done with one query instead of two, but it seems like where you're at with Ruby concepts it's more important to focus on syntax and core functionality than SQL optimization.
The correct syntax (one of several options) is:
person.activities_people.where(date: date)
I actually have this model:
class Role < ActiveRecord::Base
acts_as_authorization_role
def self.all_join_wisp
self.connection.select_all("SELECT roles.*, wisps.name AS wisp_name FROM roles LEFT JOIN wisps ON wisps.id = roles.authorizable_id")
end
end
the method all_join_wisp does almost what I want, it adds the field wisp_name, except it return a hash and not an active record object.
Is there a way to retrieve an active record object instead?
The Role model does not have belongs_to and the wisp model does not have has_many :roles , I guess it was done for flexibility or something (I did not develop the application I'm working on).
Edit: Solution implemented
Solution implemented here: https://github.com/nemesisdesign/OpenWISP-Geographic-Monitoring/blob/287861d95fffde35d78b76ca1e529c21b0f3a54b/app/models/role.rb#L25
Thanks to #house9
you can use find_by_sql - you get back what looks like an activerecord object but isn't really, it will have attributes from your query but they will all be string data types instead of the real types, often this is ok
def self.all_join_wisp
self.find_by_sql("SELECT roles.*, wisps.name AS wisp_name FROM roles LEFT JOIN wisps ON wisps.id = roles.authorizable_id")
end
then
list = Role.all_join_wisp
list.each do |x|
puts x
puts x.inspect
puts x.id # the role id
puts x.id.is_a?(Integer) # false
puts x.id.is_a?(String) # true
puts x.name # the role name
puts x.wisp_name # wisp name, NOTE: Role does not really have this attribute, find_by_sql adds it
end
model
class Role < ActiveRecord::Base
def self.do
joins("LEFT JOIN wisps w ON
w.id = roles.id").select("roles.*,
wisps.name AS wisp_name")
end
end
controller
#roles = Role.do
#wisps = #roles.map {|i| i.wisp_name}
view
<%= #wisps %>
In my Rails application I have a User model ,Department model, Group model and a Register model.User model has basic user information,
User Model:
id , name
has_and_belongs_to_many :departments , :groups
Department Model:
id,name
has_and_belongs_to_many :users
has_many :registers
Group Model:
id,name
has_and_belongs_to_many :users
has_many :registers
Register Model:
date ,department_id , group_id , one , two , three
belongs_to :department ,:group
Among the Register Model "one" , "two" , "three" are time slots say: 9-12 , 12-3 , 3-6.Every day each user has to mark attendance so that their attendance has to mark against the time slot in Register table. How to mark attendance based on current time with the time slot.
Thanks in advance.
You may need to do
current_time = Time.now
if (9..11).include?(current_time.hour)
# Do A
elsif (12..14).include?(current_time.hour)
# Do B
elsif (15..18).include?(current_time.hour)
# Do C
end
(or)
current_time = Time.now
if (9...12).include?(current_time.hour)
# Do A
elsif (12...15).include?(current_time.hour)
# Do B
elsif (15...18).include?(current_time.hour)
# Do C
end
# I think of the extra "." as 'eating' the last value so you don't have it.
to deal with the overlaps
try this
current_time = Time.now
if (9..12).include?(current_time.hour)
# 9-12
elsif (12..15).include?(current_time.hour)
# 12-3
elsif (15..18).include?(current_time.hour)
# 3-6
end
Might consider creating a bitmap. A 32 bit integer is lightweight and gives you 0-24, one for each hour. Write some functions that can compare, set,... hour ranges.
I can think of a few ways to do this, but I'm unsure what to choose..
I have the class Topic and I am trying to scope this so that I only return Topics if it has the associated object Reply or topic.replies as a count greater than 0.
Worst way to do this :
#topics.select{ | topic | topic.replies > 0 && topic.title == "Conversation" }
Ideally, I'd like to use a where scope.
scope = current_user.topics
scope = scope.joins 'left outer join users on topics.registration_id = registration_members.registration_id'
# scope = .. here I want to exclude any of these topics that have both the title "Conversations" and replies that are not greater than 0
I need to "append" these selections to anything else already selected. So my selection shouldn't exclude all others to just this selection. It's just saying that any Topic with replies less than one and also called "Conversation" should be excluded from the final return.
Any ideas?
Update
Sort of a half-hashed idea :
items_table = Arel::Table.new(scope)
unstarted_conversations = scope.select{|a| a.title == "Conversation" && a.replies.count > 0}.map(&:id)
scope.where(items_table[:id].not_in unstarted_conversations)
You can use something called count cache, basically what it does is add a field to the table and store in that field the total of "associates" of the specified type and is automatically updated.
Checkout this old screen/ascii cast: http://railscasts.com/episodes/23-counter-cache-column?view=asciicast
Here is something newer: http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
In your case would be as follow:
# migration
class AddCounterCacheToTopìc < ActiveRecord::Migration
def self.up
add_column :topics, :replies_count, :integer, :default => 0
end
def self.down
remove_column :topics, :replies_count
end
end
# model
class Replay < ActiveRecord::Base
belongs_to :topic, :counter_cache => true
end
Hope it help you.