diff --git a/app/controllers/leaderboards_controller.rb b/app/controllers/leaderboards_controller.rb new file mode 100644 index 0000000..c906eff --- /dev/null +++ b/app/controllers/leaderboards_controller.rb @@ -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 diff --git a/app/jobs/leaderboard_update_job.rb b/app/jobs/leaderboard_update_job.rb new file mode 100644 index 0000000..5e7da3f --- /dev/null +++ b/app/jobs/leaderboard_update_job.rb @@ -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 diff --git a/app/models/leaderboard.rb b/app/models/leaderboard.rb new file mode 100644 index 0000000..77933f9 --- /dev/null +++ b/app/models/leaderboard.rb @@ -0,0 +1,7 @@ +class Leaderboard < ApplicationRecord + has_many :entries, + class_name: "LeaderboardEntry", + dependent: :destroy + + validates :start_date, presence: true, uniqueness: true +end diff --git a/app/models/leaderboard_entry.rb b/app/models/leaderboard_entry.rb new file mode 100644 index 0000000..b610ed2 --- /dev/null +++ b/app/models/leaderboard_entry.rb @@ -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 diff --git a/app/views/leaderboards/index.html.erb b/app/views/leaderboards/index.html.erb new file mode 100644 index 0000000..0384726 --- /dev/null +++ b/app/views/leaderboards/index.html.erb @@ -0,0 +1,37 @@ +
+
+

Today's Leaderboard

+

+ <%= Date.current.strftime("%B %d, %Y") %> + Updated <%= time_ago_in_words(@leaderboard.updated_at) %> ago +

+
+ +
+ <% if @entries.any? %> + + + + + + + + + + <% @entries.each_with_index do |entry, index| %> + + + + + + <% end %> + +
RankUserTime
<%= (index + 1).ordinalize %><%= render "shared/user_mention", user: entry.user %><%= Time.at(entry.total_seconds).utc.strftime("%H:%M:%S") %>
+ <% else %> +
+

No data available

+

Check back later for today's results!

+
+ <% end %> +
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 3c6d041..d60f833 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 0000000..b74d24b --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,3 @@ +every 1.hour do + runner "UpdateLeaderboardJob.perform_later" +end diff --git a/db/migrate/20240316000001_create_daily_leaderboard_entries.rb b/db/migrate/20240316000001_create_daily_leaderboard_entries.rb new file mode 100644 index 0000000..d04979a --- /dev/null +++ b/db/migrate/20240316000001_create_daily_leaderboard_entries.rb @@ -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 diff --git a/db/migrate/20240316000002_rename_daily_leaderboard_tables.rb b/db/migrate/20240316000002_rename_daily_leaderboard_tables.rb new file mode 100644 index 0000000..553cf6d --- /dev/null +++ b/db/migrate/20240316000002_rename_daily_leaderboard_tables.rb @@ -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 diff --git a/db/migrate/20240316000003_remove_status_from_leaderboards.rb b/db/migrate/20240316000003_remove_status_from_leaderboards.rb new file mode 100644 index 0000000..1b7dbb6 --- /dev/null +++ b/db/migrate/20240316000003_remove_status_from_leaderboards.rb @@ -0,0 +1,5 @@ +class RemoveStatusFromLeaderboards < ActiveRecord::Migration[8.0] + def change + remove_column :leaderboards, :status, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 016c19a..14a17b7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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