Add daily leaderboard

This commit is contained in:
Max Wofford 2025-02-17 19:05:30 -05:00
parent 8eea8efb98
commit 499036038e
11 changed files with 154 additions and 2 deletions

View file

@ -0,0 +1,9 @@
class LeaderboardsController < ApplicationController
def index
@leaderboard = Leaderboard.find_by(start_date: Date.current)
@entries = @leaderboard.entries
.includes(:user)
.order(total_seconds: :desc)
end
end

View file

@ -0,0 +1,36 @@
class LeaderboardUpdateJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: :date, duration: 5.minutes
def perform(date = nil, leaderboard = nil)
if !leaderboard
date ||= Date.current
leaderboard = Leaderboard.find_or_initialize_by(start_date: date)
end
ActiveRecord::Base.transaction do
# Reset the leaderboard to recalculate
# leaderboard.calculating!
leaderboard.entries.destroy_all
begin
User.find_each do |user|
seconds = user.heartbeats.where("DATE(time) = ?", date).duration_seconds
next if seconds.zero?
leaderboard.entries.build(
user_id: user.slack_uid,
total_seconds: seconds
)
end
leaderboard.touch(:updated_at) unless leaderboard.new_record?
leaderboard.save!
rescue => e
Rails.logger.error "Failed to update current leaderboard: #{e.message}"
# leaderboard.failed!
raise
end
end
end
end

View file

@ -0,0 +1,7 @@
class Leaderboard < ApplicationRecord
has_many :entries,
class_name: "LeaderboardEntry",
dependent: :destroy
validates :start_date, presence: true, uniqueness: true
end

View file

@ -0,0 +1,8 @@
class LeaderboardEntry < ApplicationRecord
belongs_to :leaderboard
belongs_to :user, primary_key: :slack_uid
validates :user_id, presence: true
validates :total_seconds, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :user_id, uniqueness: { scope: :leaderboard_id }
end

View file

@ -0,0 +1,37 @@
<div class="leaderboard">
<div class="header">
<h1>Today's Leaderboard</h1>
<p class="date">
<%= Date.current.strftime("%B %d, %Y") %>
<em>Updated <%= time_ago_in_words(@leaderboard.updated_at) %> ago</em>
</p>
</div>
<div class="content">
<% if @entries.any? %>
<table>
<thead>
<tr>
<th>Rank</th>
<th>User</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<% @entries.each_with_index do |entry, index| %>
<tr>
<td><%= (index + 1).ordinalize %></td>
<td><%= render "shared/user_mention", user: entry.user %></td>
<td><%= Time.at(entry.total_seconds).utc.strftime("%H:%M:%S") %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<div class="empty-state">
<h3>No data available</h3>
<p>Check back later for today's results!</p>
</div>
<% end %>
</div>
</div>

View file

@ -17,4 +17,6 @@ Rails.application.routes.draw do
get "/auth/slack", to: "sessions#new", as: :slack_auth
get "/auth/slack/callback", to: "sessions#create"
delete "signout", to: "sessions#destroy", as: "signout"
resources :leaderboards, only: [ :index ]
end

3
config/schedule.rb Normal file
View file

@ -0,0 +1,3 @@
every 1.hour do
runner "UpdateLeaderboardJob.perform_later"
end

View file

@ -0,0 +1,13 @@
class CreateDailyLeaderboardEntries < ActiveRecord::Migration[8.0]
def change
create_table :daily_leaderboard_entries do |t|
t.references :daily_leaderboard, null: false, foreign_key: true
t.string :user_id, null: false
t.integer :total_seconds, null: false, default: 0
t.integer :rank
t.timestamps
t.index [ :daily_leaderboard_id, :user_id ], unique: true, name: 'idx_leaderboard_entries_on_leaderboard_and_user'
end
end
end

View file

@ -0,0 +1,9 @@
class RenameDailyLeaderboardTables < ActiveRecord::Migration[8.0]
def change
rename_table :daily_leaderboards, :leaderboards
rename_table :daily_leaderboard_entries, :leaderboard_entries
# Update the foreign key
rename_column :leaderboard_entries, :daily_leaderboard_id, :leaderboard_id
end
end

View file

@ -0,0 +1,5 @@
class RemoveStatusFromLeaderboards < ActiveRecord::Migration[8.0]
def change
remove_column :leaderboards, :status, :integer
end
end

27
db/schema.rb generated
View file

@ -11,6 +11,27 @@
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_02_16_173459) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
create_table "leaderboard_entries", force: :cascade do |t|
t.bigint "leaderboard_id", null: false
t.string "user_id", null: false
t.integer "total_seconds", default: 0, null: false
t.integer "rank"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["leaderboard_id", "user_id"], name: "idx_leaderboard_entries_on_leaderboard_and_user", unique: true
t.index ["leaderboard_id"], name: "index_leaderboard_entries_on_leaderboard_id"
end
create_table "leaderboards", force: :cascade do |t|
t.date "start_date", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["start_date"], name: "index_leaderboards_on_start_date", unique: true
end
create_table "users", force: :cascade do |t|
t.string "slack_uid", null: false
t.string "email", null: false
@ -28,8 +49,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_173459) do
t.bigint "item_id", null: false
t.string "item_type", null: false
t.string "event", null: false
t.text "object", limit: 1073741823
t.text "object_changes", limit: 1073741823
t.text "object"
t.text "object_changes"
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end
add_foreign_key "leaderboard_entries", "leaderboards"
end