diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 5d2d0584..7396d500 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -21,6 +21,7 @@ def calendar } end + @daily_buddy = Buddy.of_the_day(current_user) @next_puzzle_time = Aoc.next_puzzle_time @now = Aoc.event_timezone.now end diff --git a/app/jobs/buddies/generate_daily_pairs_job.rb b/app/jobs/buddies/generate_daily_pairs_job.rb new file mode 100644 index 00000000..e3148064 --- /dev/null +++ b/app/jobs/buddies/generate_daily_pairs_job.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Buddies + class GenerateDailyPairsJob < ApplicationJob + queue_as :default + + def perform(day) + @day = day + + if Buddy.exists?(day:) + Rails.logger.info "Daily pairs already exist for day #{day}" + return + end + + retrieve_confirmed_users + handle_odd_number_of_users + generate_possible_pairs_of_buddies + + @possible_pairs.shuffle! + + pick_valid_pairs_of_buddies + handle_unpaired_users + + insert_generated_buddies + end + + private + + def retrieve_confirmed_users + @user_ids = User.confirmed.order(:id).pluck(:id) + end + + def handle_odd_number_of_users + @user_ids.pop if @user_ids.size.odd? + end + + def generate_possible_pairs_of_buddies + all_pairs = @user_ids.combination(2).to_set + past_buddies = Buddy.pluck(:id_1, :id_2).to_set + + @possible_pairs = (all_pairs - past_buddies).to_a + end + + def pick_valid_pairs_of_buddies + @users_to_match = Set.new(@user_ids) + @buddies = [] + + # Iterate once over possible pairs to find matches + @possible_pairs.each do |pair| + # If a pair contains two available IDs, it's a match! + if pair.all? { |id| @users_to_match.include?(id) } + @buddies << pair + pair.each { |id| @users_to_match.delete(id) } + end + end + end + + def handle_unpaired_users + @buddies += @users_to_match.to_a.shuffle.each_slice(2).to_a + end + + def insert_generated_buddies + Buddy.insert_all!(@buddies.map { |a, b| { day: @day, id_1: a, id_2: b } }) + Rails.logger.info "Buddies successfully generated for day #{@day}" + end + end +end diff --git a/app/models/buddy.rb b/app/models/buddy.rb new file mode 100644 index 00000000..000541aa --- /dev/null +++ b/app/models/buddy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Buddy < ApplicationRecord + scope :of_today, -> { where(day: Aoc.latest_day) } + scope :of_user, ->(user_id) { where("id_1 = ? OR id_2 = ?", user_id, user_id) } + + def self.of_the_day(user) + buddy_pair = of_today.of_user(user.id).first + daily_buddy_id = user.id == buddy_pair.id_1 ? buddy_pair.id_2 : buddy_pair.id_1 if buddy_pair + + User.find_by(id: daily_buddy_id) + end +end diff --git a/app/views/pages/calendar.html.erb b/app/views/pages/calendar.html.erb index e96d397d..b5c58327 100644 --- a/app/views/pages/calendar.html.erb +++ b/app/views/pages/calendar.html.erb @@ -2,6 +2,20 @@ Welcome, <%= link_to current_user.username, profile_path(current_user.uid), class: "strong hover:text-gold" %>

+<% if @daily_buddy.present? %> + +

+ Your daily buddy is + + <% if @daily_buddy.slack_id.present? %> + <%= link_to "@#{@daily_buddy.slack_username}", @daily_buddy.slack_link, target: :_blank, rel: :noopener, class: "link-explicit link-slack" %> + <% else %> + <%= link_to @daily_buddy.username, profile_path(@daily_buddy.uid), class: "link-explicit" %> + <% end %> +

+ +<% end %> +
<%= render Calendar::AdventDayComponent.with_collection(@advent_days, now: @now) %>
diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb index 14b3afdc..d3c6d908 100644 --- a/config/initializers/good_job.rb +++ b/config/initializers/good_job.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "aoc" + Rails.application.configure do config.good_job = { execution_mode: :external, @@ -7,16 +9,21 @@ shutdown_timeout: 30, enable_cron: true, cron: { - refresh_completions: { - cron: "*/10 * 1-30 11-12 *", # every 10th minute between November 1st and December 30th + refresh_completions: { # every 10 minutes between November 1st and December 30th + cron: "*/10 * 1-30 11-12 *", class: "InsertNewCompletionsJob" }, - auto_cleanup: { - cron: "55 5 * * *", # every day at 5:55 + auto_cleanup: { # every puzzle day, just before a new puzzle + cron: "55 23 1-25 12 * America/New_York", class: "Cache::CleanupJob" }, - lock_time_achievements: { - cron: "30 18 9 12 *", # 9th December at 18:30 + generate_buddies: { # every puzzle day, just after a new puzzle + cron: "5 0 1-25 12 * America/New_York", + class: "Buddies::GenerateDailyPairsJob", + args: [Aoc.latest_day] + }, + unlock_lock_time_achievements: { # once at lock time + cron: "30 17 8 12 * Europe/Paris", class: "Achievements::LockTimeJob" } } diff --git a/db/migrate/20231201004728_create_buddies.rb b/db/migrate/20231201004728_create_buddies.rb new file mode 100644 index 00000000..ce51fea2 --- /dev/null +++ b/db/migrate/20231201004728_create_buddies.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateBuddies < ActiveRecord::Migration[7.1] + def change + create_table :buddies do |t| + t.integer :id_1 + t.integer :id_2 + t.integer :day + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 17875e22..1716c533 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_11_30_162723) do +ActiveRecord::Schema[7.1].define(version: 2023_12_01_004728) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pgcrypto" @@ -89,6 +89,14 @@ t.index ["creator_id"], name: "index_blazer_queries_on_creator_id" end + create_table "buddies", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "day" + t.integer "id_1" + t.integer "id_2" + t.datetime "updated_at", null: false + end + create_table "cities", force: :cascade do |t| t.datetime "created_at", null: false t.string "name"