Logic and code help - ruby-on-rails

Here are my models and associations:
User has many Awards
Award belongs to User
Prize has many Awards
Award belongs to Prize
Let's pretend that there are four Prizes (captured as records):
Pony
Toy
Gum
AwesomeStatus
Every day a User can be awarded one or more of these Prizes. But the User can only receive each Prize once per day. If the User wins AwesomeStatus, for ex, a record is added to the Awards table with a fk to User and Prize. Obviously, if the User doesn't win the AwesomeStatus for the day, no record is added.
At the end of the day (before midnight, let's say), I want to return a list of Users who lost their AwesomeStatus. (Of course, to lose your AwesomeStatus, you had to have the day before.) Unfortunately, in my case, I don't think observers will work and will have to rely on a script. Regardless, how would you go about determining which Users lost their AwesomeStatus? Note: don't make your solution overly dependent on the period of time -- in this case a day. I want to maintain flexibility in how many times per whatever period Users have an opportunity to win the prize (and to also lose it).

I would probably do something like this:
The class Award should also have a column awarded_at which contains the day the prize was awarded. So when it is time to create the award it can be done like this:
# This will make sure that no award will be created if it already exists for the current date
#user.awards.find_or_create_by_prize_id_and_awarded_at(#prize.id, Time.now.strftime("%Y-%m-%d"))
And then we can have a scope to load all users with an award that will expire today and no active awards for the supplied prize.
# user.rb
scope :are_losing_award, lambda { |prize_id, expires_after|
joins("INNER JOIN awards AS expired_awards ON users.id = expired_awards.user_id AND expired_awards.awarded_at = '#{(Time.now - expires_after.days).strftime("%Y-%m-%d")}'
LEFT OUTER JOIN awards AS active_awards ON users.id = active_awards.user_id AND active_awards.awarded_at > '(Time.now - expires_after.days).strftime("%Y-%m-%d")}' AND active_awards.prize_id = #{prize_id}").
where("expired_awards.prize_id = ? AND active_awards.id IS NULL", prize_id)
}
So then we can call it like this:
# Give me all users who got the prize three days ago and has not gotten it again since
User.are_losing_award(#prize.id, 3)
There might be some ways to write the scope better with ARel queries or something, I'm no expert with that yet, but this way should work until then :)

I'd add an integer "time period" field to awards, which stands for a given period of time (day, week, 5 hour period, whatever you want).
Now, you can search the awards table for users who have the award status at t-1, but not at t:
SELECT prev.user_id
FROM awards prev
OUTER JOIN awards current ON prev.user_id = current.user_id
AND prev.prize_id = current.prize_id
AND current.time_period = 1000
WHERE prev.prize_id = 1
AND current.prize_id IS NULL
AND prev.time_period = 999

Just use updated_at, or add an awarded_at like suggested above and use it like this:
scope :awarded, proc {|date| where(["updated_at <= ?", date])}
In your Award model. Print it like this, maybe:
awesome_status = Prize.find_by_name('AwesomeStatus')
p "Users who do not have AwesomeStatus anymore:"
User.all.each {|user| p user.username if user.awards.awarded(1.day.ago).collect(&:id).include?(awesome_status)}
If you want it to be dynamic, displayed somewhere, etc. throw a 'lasts_for' into Prize and compare against it and simply write a maintenance cronjob that sets an 'active' boolean on Award to false instead of deleting the association.

Related

How to show all records from multiple tables regardless of match on join statement

I am trouble figuring out the proper syntax to structure this query correctly. I am trying to show ALL records from both the SalesHistoryDetail AND from the SalesVsBudget table. I believe my query allows for some of the records on SalesVsBudget to not be pulled, whereas I want them all for that period, regardless of whether there was a corresponding sale. Here is my code:
SELECT MAX(a.DispatchCenterOrderKey) AS DispatchCenter,
a.CustomerKey,
CASE WHEN a.CustomerKey IN
(SELECT AddressKey
FROM FinancialData.dbo.DimAddress
WHERE AddressKey >= 99000 AND AddressKey <= 99599) THEN 1 ELSE 0 END AS InterCompanyFlag,
MAX(a.Customer) AS Customer,
a.SalesmanID,
MAX(a.Salesman) AS Salesman,
a.SubCategoryKey,
MAX(a.SubCategoryDesc) AS Subcategory,
SUM(a.Value) AS SalesAmt,
b.FiscalYear AS Year,
b.FiscalWeekOfYear AS Week,
MAX(c.BudgetLbs) AS BudgetLbs,
MAX(c.BudgetDollars) AS BudgetDollars
FROM dbo.SalesHistoryDetail AS a
LEFT OUTER JOIN dbo.M_DateDim AS b ON a.InvoiceDate = b.Date
FULL OUTER JOIN dbo.SalesVsBudget AS c ON a.SalesmanID = c.SalesRepKey
AND a.CustomerKey = c.CustomerKey
AND a.SubCategoryKey = c.SubCategoryKey
AND b.FiscalYear = c.Year AND b.FiscalWeekOfYear = c.WeekNo
GROUP BY a.SalesmanID, a.CustomerKey, a.SubCategoryKey, b.FiscalYear, b.FiscalWeekOfYear
There are two different data sets that I am pulling from, obviously the SalesHistoryDetail table and the SalesVsBudget table. I'm hoping to get ALL budgetLbs, and BudgetDollars values from the SalesVsBudget table regardless of whether they match in the join. I want all of the matching joining records too, but I also want EVERY record from SalesVsBudget. Essentially I want to show ALL sales records and I want to reference the budget values from SalesVsBudget when the salesman,customer,subcategory, year and week match but I also want to see budget entries that fall in my date range that don't have corresponding sales records in that period. Hopefully that makes sense. I feel I am very close, but my budget numbers doesn't reflect the whole story and I think that is because some of my records are being excluded! Please help.
I was able to accomplish this through playing with the FULL OUTER JOIN. My problems was there were more records in SalesVsBudget than SalesHistory_V. Therefore I had to make SalesVsBudget the initial FROM table and SaleHistory_V with a FULL OUTER JOIN and all records lined up.

Rails Active Record - Calling Distict Rows

I have the following models
influencers (id, name, ..)
influencer_authorizations (id, influencer_id, ... )
influencer_metrics (id, influencer_authorization_id, date, count)
The relationships are as follows
influencers has_many influencer_authorizations
influencer_authorizations has_many influencer_metrics
I want to fetch the count of all the influencer_authorizations in the influencer_metrics with the latest DATE and sum to show the total followers of the influencer
What am trying
Finding all the Authorizations IDs
influencer_authorizations = InfluencerAuthorization.where(influencer_id: influencer.id).pluck(:id)
Now i am trying to find the latest Date's rows all the influencer_authorizations technically for each influencer_authorizations it should give back one row whose sum i want to calculate.
total_followers = InfluencerMetric.where(influencer_authorization_id: influencer_authorizations).order('metrics_date DESC').first
But it returns all the rows right now. The date is tricky as i each influencer_authorizations last fetched date can be different at times. So we need the lastest metrics records for each influencer_authorizations
Could this possibly be what you're looking for?
Influencer.
joins(influencer_authorizations: :influencer_metrics).
select("SUM(influencer_metrics.count) as followers, COUNT(influencer_metrics.id)").
where(id: influencer.id).
group("influencer_authorizations.id").
order("influencer_metrics.metrics_date DESC").
limit(1)
I'm not quite clear on the query that you described, as some of the wording seemed inverted from the schema relationships. It may be a starting point.
If it happens to not work as expected, try to remove the .limit(1) portion and see what's returned. We can go from there if you're game.

Report using Rails ActiveRecord group by

I am trying to generate a report to screen of accounting transaction history. In most situations it is one display row per record in the AccountingTransaction table. But occasionally there are transactions that I wish to display to the end user as one transaction which are really, behind the scenes, two accounting transactions. This is caused by deferral of revenues and fund splitting since this app is a fund accounting app.
If I display all rows one by one, those double entries look odd to the user since the fund splitting and deferral is "behind the scenes". So I want to roll up all the related transactions into one display row on screen.
I have my query now using group by to group the related transactions
#history = AccountingTransaction.where("customer_id in (?) AND no_download <> 1", customers_in_account).group(:transaction_type_id, :reference_id).order(:created_at)
as I loop through I get the transactions grouped as I want but I am struggling with how to display the total sum of the 'credit' field for all records in the group. (It is only showing the credit for the first record of the group) If I add a .sum(:credit) to my query, of course, it returns the sums just as I want but not all the other data.
Is there a way for me to group these records like in my #history query and also get the sum of the credit field for each respective group?
* Addition *
What I really want is what the following SQL query would give me.
SELECT transaction_type_id, reference_id, sum(credit)
WHERE customer_id in (21,22,23,24) AND no_download <> 1
GROUP BY reference_id, transaction_type_id ORDER BY created_at
I'm not sure you can do "ORDER BY created_at" and not include it in the select fields, but here is an example.
#history = AccountingTransaction.
select([:reference_id, :transaction_type_id, :created_at]).
select(AccountingTransaction.arel_table[:credit].sum.as("credit_sum")).
where("customer_id in (?) AND no_download <> 1", customers_in_account).
group(:transaction_type_id, :reference_id).
order(:created_at)
To access the credit_sum you could do:
#history[0].attributes["credit_sum"]
I guess if you'd like, you could create a method:
def credit_sum
attributes["credit_sum"]
end
EDIT *
As stated in comments you can access the attribute directly:
#history[0].credit_sum

Get all comments by a set of users made the 24 hours prior to their last comment

I have a many-to-one relationship between the models User and Comment.
I would like to collect, preferably in a hash, all comments made by a set of users the 24 hours prior to each user's last comment (including his/her last comment).
This is what I have come up with, but I dont know how to create the hash with comments only from the time span mentioned.
Comment.order('updated_at desc').where(user_id: array_of_users_ids).group_by(&:user).each do |user, comments|
# rearrange the hash here?
end
The SQL solution for this would be a correlated subquery, made slightly tricky by non-standard date arithmetic depending on the RDBMS in use.
You'd be looking for a query such as:
select ...
from comments
where comments.user_id in (...) and
comments.updated_at >= (
select max(updated_at) - interval '1 day'
from comments c2
where c2.user_id = comments.user_id)
You ought to be able to achieve this with:
Comment.where(user_id: array_of_users_ids).
where("comments.updated_at >= (
select max(updated_at) - interval '1 day'
from comments c2
where c2.user_id = comments.user_id)").
order(... etc

Ranking position

I have a Rails application with the following models:
User
Bet
User has many_bets and Bets belongs_to User. Every Bet has a Profitloss value, which states how much the User has won/lost on that Bet.
So to calculate how much a specific User has won overall I cycle through his bets in the following way:
User.bets.sum(:profitloss)
I would like to show the User his ranking compared to all the other Users, which could look something like this:
"Your overall ranking: 37th place"
To do so I need to sum up the overall winnings per User, and find out in which position the current user is.
How do I do that and how to do it, so it don't overload the server :)
Thanks!
You can try something similar to
User.join(:bets).
select("users.id, sum(bets.profitloss) as accumulated").
group("users.id").
order("accumulated DESC")
and then search in the resulting list of "users" (not real users, they have only two meaningful attributes, their ID and a accumulated attribute with the sum), for the one corresponding to the current one.
In any case to get a single user's position, you have to calculate all users' accumulated, but at least this is only one query. Even better, you can store in the user model the accumulated value, and query just it for ranking.
If you have a large number of Users and Bets, you won't be able to compute and sort the global profitloss of each user "on demand", so I suggest that you use a rake task that you schedule regularly (once a day, every hour, etc...)
Add a column position in the User model, get the list of all Users, compute their global profitloss, sort the list of Users with their profitloss, and finally update the position attribute of each User with their position in the list.
Best way to do it is to keep a pre calculated total in your database either on user model itself or on a separate model that has 1:1 relation to user. If you don't do this, you will have to calculate sum for all users at all times in order to get their rating, which means full table operation on bets table. This said, this query will give you desired results, if more than 1 person has the same total, it will count both as rating X:
select id, (select count(h.id) from users u inner join
(select user_id, sum(profitloss) as `total` from bets group by user_id) b2
on b2.user_id = u.id, (select id from users) h inner join
(select user_id, sum(profitloss) as `total` from bets group by user_id) b
on b.user_id = h.id where u.id = 1 and (b.total > b2.total))
as `rating` from users where id = 1;
You will need to plug user.id into query in where id = X
if you add a column to user table to keep track of their total, query is a little simpler, in this example column name is total_profit_loss:
select id, total_profit_loss, (select count(h.username)+1 from users u,
(select username, score from users) h
where id = 1 and (h.total_profit_loss > u.total_profit_loss))
as `rating` from users where id = 1;

Resources