Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Badges for 48in24 #7207

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def status(exercise)
completed_exercises = completions[exercise[:slug]].to_a
return :in_progress if completed_exercises.blank?

num_completions_in_2024 = completed_exercises.count { |(_, year)| year == 2024 }
num_completions_in_2024 = completed_exercises.count { |(_, date)| date >= '2023-12-31' && date <= '2025-01-01' }
return :in_progress if num_completions_in_2024.zero?
return :bronze if num_completions_in_2024 < 3

Expand All @@ -86,7 +86,7 @@ def completions
user.solutions.completed.
joins(exercise: :track).
where(exercise: { slug: EXERCISES.pluck(:slug) }).
pluck('exercise.slug', 'tracks.slug', 'YEAR(completed_at)').
pluck('exercise.slug', 'tracks.slug', Arel.sql("DATE_FORMAT(completed_at, '%Y-%m-%d')")).
group_by(&:first).
transform_values { |entries| entries.map { |entry| entry[1..] } }
end
Expand Down
15 changes: 15 additions & 0 deletions app/models/badges/larisa_latynina_badge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Badges
class LarisaLatyninaBadge < Badge
seed "Larisa Latynina",
:rare,
'larisa-latynina',
'Earned 48 medals in the #48in24 challenge'

def award_to?(user)
exercises = User::Challenges::FeaturedExercisesProgress48In24.(user)
exercises.none? { |e| e.status == :in_progress }
end

def send_email_on_acquisition? = true
end
end
16 changes: 16 additions & 0 deletions app/models/badges/paavo_nurmi_badge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Badges
class PaavoNurmiBadge < Badge
seed "Paavo Nurmi",
:ultimate,
'paavo-nurmi',
'Earned 48 gold or silver medals in the #48in24 challenge'

def award_to?(user)
exercises = User::Challenges::FeaturedExercisesProgress48In24.(user)
exercises.none? { |e| e.status == :in_progress } &&
exercises.none? { |e| e.status == :bronze }
end

def send_email_on_acquisition? = true
end
end
15 changes: 15 additions & 0 deletions app/models/badges/participant_in_48_in_24_badge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Badges
class ParticipantIn48In24Badge < Badge
seed "#48in24 Participant",
:common,
'48_in_24',
'Participated in the #48in24 challenge and achieved a medal'

def award_to?(user)
exercises = User::Challenges::FeaturedExercisesProgress48In24.(user)
exercises.any? { |e| e.status != :in_progress }
end

def send_email_on_acquisition? = true
end
end
15 changes: 15 additions & 0 deletions app/models/badges/usain_bolt_badge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Badges
class UsainBoltBadge < Badge
seed "Usain Bolt",
:legendary,
'usain-bolt',
'Earned 48 gold medals in the #48in24 challenge'

def award_to?(user)
exercises = User::Challenges::FeaturedExercisesProgress48In24.(user)
exercises.all? { |e| e.status == :gold }
end

def send_email_on_acquisition? = true
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ class User::Challenges::FeaturedExercisesProgress48In24Test < ActiveSupport::Tes
test "returns completed tracks" do
user = create :user

create_completed_solution(user, 2022, 'reverse-string', 'cpp')
create_completed_solution(user, 2023, 'reverse-string', 'nim')
create_completed_solution(user, 2024, 'leap', 'elixir')
create_completed_solution(user, 2022, 2, 3, 'reverse-string', 'cpp')
create_completed_solution(user, 2023, 4, 5, 'reverse-string', 'nim')
create_completed_solution(user, 2024, 6, 7, 'leap', 'elixir')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)

exercise_progress = progress_by_exercise(progress)
assert_equal ({ "cpp" => 2022, "nim" => 2023 }), exercise_progress["reverse-string"].completed_tracks
assert_equal ({ "elixir" => 2024 }), exercise_progress["leap"].completed_tracks
assert_equal ({ "cpp" => '2022-02-03', "nim" => '2023-04-05' }), exercise_progress["reverse-string"].completed_tracks
assert_equal ({ "elixir" => '2024-06-07' }), exercise_progress["leap"].completed_tracks
end

test "gold status when user has completed all three featured tracks in 2024" do
user = create :user

create_completed_solution(user, 2024, 'reverse-string', 'cpp')
create_completed_solution(user, 2024, 'reverse-string', 'nim')
create_completed_solution(user, 2024, 'reverse-string', 'javascript')
create_completed_solution(user, 2024, 11, 12, 'reverse-string', 'cpp')
create_completed_solution(user, 2024, 10, 11, 'reverse-string', 'nim')
create_completed_solution(user, 2024, 12, 13, 'reverse-string', 'javascript')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)

Expand All @@ -50,24 +50,24 @@ class User::Challenges::FeaturedExercisesProgress48In24Test < ActiveSupport::Tes
test "gold status when completed all featured tracks and at least three iterations in 2024" do
user = create :user

create_completed_solution(user, 2021, 'reverse-string', 'cpp')
create_completed_solution(user, 2022, 'reverse-string', 'csharp')
create_completed_solution(user, 2023, 'reverse-string', 'javascript')
create_completed_solution(user, 2021, 4, 5, 'reverse-string', 'cpp')
create_completed_solution(user, 2022, 6, 7, 'reverse-string', 'csharp')
create_completed_solution(user, 2023, 8, 9, 'reverse-string', 'javascript')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)

exercise_progress = progress_by_exercise(progress)
assert_equal :in_progress, exercise_progress["reverse-string"].status

# Create two iterations in 2024
create_completed_solution(user, 2024, 'reverse-string', 'zig')
create_completed_solution(user, 2024, 'reverse-string', 'nim')
create_completed_solution(user, 2024, 1, 10, 'reverse-string', 'zig')
create_completed_solution(user, 2024, 2, 20, 'reverse-string', 'nim')

exercise_progress = progress_by_exercise(progress)
assert_equal :in_progress, exercise_progress["reverse-string"].status

# Ensure that there are now three iterations in 2024
create_completed_solution(user, 2024, 'reverse-string', 'racket')
create_completed_solution(user, 2024, 3, 30, 'reverse-string', 'racket')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)
exercise_progress = progress_by_exercise(progress)
Expand All @@ -77,18 +77,18 @@ class User::Challenges::FeaturedExercisesProgress48In24Test < ActiveSupport::Tes
test "silver status when user has completed at least three tracks in 2024 (but not the three featured ones)" do
user = create :user

create_completed_solution(user, 2024, 'reverse-string', 'zig')
create_completed_solution(user, 2024, 'reverse-string', 'csharp')
create_completed_solution(user, 2024, 'reverse-string', 'nim')
create_completed_solution(user, 2024, 1, 1, 'reverse-string', 'zig')
create_completed_solution(user, 2024, 2, 2, 'reverse-string', 'csharp')
create_completed_solution(user, 2024, 3, 3, 'reverse-string', 'nim')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)

exercise_progress = progress_by_exercise(progress)
assert_equal :silver, exercise_progress["reverse-string"].status

# Even if the user has started all three featured tracks, they don't count if they're not completed
create_non_completed_solution(user, 2024, 'reverse-string', 'python')
create_non_completed_solution(user, 2024, 'reverse-string', 'javascript')
create_non_completed_solution(user, 2024, 4, 4, 'reverse-string', 'python')
create_non_completed_solution(user, 2024, 5, 5, 'reverse-string', 'javascript')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)

Expand All @@ -98,7 +98,7 @@ class User::Challenges::FeaturedExercisesProgress48In24Test < ActiveSupport::Tes

test "bronze status when user has completed at least one track in 2024" do
user = create :user
create_completed_solution(user, 2024, 'reverse-string', 'kotlin')
create_completed_solution(user, 2024, 6, 6, 'reverse-string', 'kotlin')

progress = User::Challenges::FeaturedExercisesProgress48In24.(user.reload)

Expand All @@ -109,16 +109,16 @@ class User::Challenges::FeaturedExercisesProgress48In24Test < ActiveSupport::Tes
private
def progress_by_exercise(progress) = progress.index_by(&:slug)

def create_completed_solution(user, year, exercise_slug, track_slug)
travel_to Time.utc(year, SecureRandom.random_number(1..12), SecureRandom.random_number(1..28)) do
def create_completed_solution(user, year, month, day, exercise_slug, track_slug)
travel_to Time.utc(year, month, day) do
track = create(:track, slug: track_slug)
exercise = create(:practice_exercise, slug: exercise_slug, track:)
create(:practice_solution, :completed, user:, exercise:)
end
end

def create_non_completed_solution(user, year, exercise_slug, track_slug)
travel_to Time.utc(year, SecureRandom.random_number(1..12), SecureRandom.random_number(1..28)) do
def create_non_completed_solution(user, year, month, day, exercise_slug, track_slug)
travel_to Time.utc(year, month, day) do
track = create(:track, slug: track_slug)
exercise = create(:practice_exercise, slug: exercise_slug, track:)
create(:practice_solution, user:, exercise:)
Expand Down
1 change: 1 addition & 0 deletions test/factories/badges.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
summer_of_sexps jurassic_july apps_august slimline_september
object_oriented_october nibbly_november december_diversions
completed_12_in_23 polyglot
participant_in_48_in_24 larisa_latynina paavo_nurmi usain_bolt
].each do |type|
factory "#{type}_badge", class: "Badges::#{type.to_s.camelize}Badge" do
end
Expand Down
90 changes: 90 additions & 0 deletions test/models/badges/larisa_latynina_badge_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require "test_helper"

class Badge::LarisaLatyninaBadgeTest < ActiveSupport::TestCase
test "attributes" do
badge = create :larisa_latynina_badge
assert_equal "Larisa Latynina", badge.name
assert_equal :rare, badge.rarity
assert_equal :'larisa-latynina', badge.icon
assert_equal 'Earned 48 medals in the #48in24 challenge', badge.description
assert badge.send_email_on_acquisition?
assert_nil badge.notification_key
end

test "award_to?" do
badge = create :larisa_latynina_badge
exercises = User::Challenges::FeaturedExercisesProgress48In24::EXERCISES
week_1 = exercises.find { |e| e[:week] == 1 }
tracks = exercises.map { |e| e[:featured_tracks] }.flatten.uniq.
map { |track_slug| [track_slug.to_sym, create(:track, slug: track_slug)] }.
to_h
user = create :user

# No solutions
create :user_challenge, user:, challenge_id: '48in24'
refute badge.award_to?(user.reload), "new user has no medals"

# One bronze all year
exercise = create(:practice_exercise, track: tracks[:csharp], slug: week_1[:slug])
create(:practice_solution, :published, user:, track: tracks[:csharp], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
refute badge.award_to?(user.reload), "one bronze does not qualify"

# One silver medal: csharp+tcl+wren are never the featured tracks for an exercise
%i[tcl wren].each do |track_slug|
exercise = create(:practice_exercise, track: tracks[track_slug], slug: week_1[:slug])
create(:practice_solution, :published, user:, track: tracks[track_slug], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
refute badge.award_to?(user.reload), "one silver does not qualify"

# One gold medal
week_1[:featured_tracks].each do |track_slug|
exercise = create(:practice_exercise, track: tracks[track_slug.to_sym], slug: week_1[:slug])
create(:practice_solution, :published, user:, track: tracks[track_slug.to_sym], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
refute badge.award_to?(user.reload), "one gold does not qualify"

# in addition to week 1 gold, add 46 bronze medals: 47 medals does not qualify
exercises.reject { |e| e[:week] == 1 || e[:week] == 48 }.each do |e|
exercise = create(:practice_exercise, track: tracks[:csharp], slug: e[:slug])
create(:practice_solution, :published, user:, track: tracks[:csharp], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
refute badge.award_to?(user.reload), "47 medals does not qualify"

# add week 48 solution, not in 2024
week_48 = exercises.find { |e| e[:week] == 48 }
exercise = create(:practice_exercise, track: tracks[:csharp], slug: week_48[:slug])
solution = create(:practice_solution, :published, user:, track: tracks[:csharp], exercise:,
completed_at: Time.utc(2023, SecureRandom.rand(1..11), SecureRandom.rand(1..28)))
refute badge.award_to?(user.reload), "47 medals plus one solution in 2023 does not qualify"

# change completion date to 2024 to qualify
solution.update(completed_at: Time.utc(2024, 1, 1, 0, 0, 0))
assert badge.award_to?(user.reload), "48 bronzes qualifies"

# 48 gold or silver medals
exercises.reject { |e| e[:week] == 1 }.each do |e|
%i[tcl wren].each do |t|
exercise = create(:practice_exercise, track: tracks[t], slug: e[:slug])
create(:practice_solution, :published, user:, track: tracks[t], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
end
assert badge.award_to?(user.reload), "48 golds&silvers qualifies"

# 48 gold medals
exercises.reject { |e| e[:week] == 1 }.each do |e|
e[:featured_tracks].map(&:to_sym).each do |t|
next if %i[csharp tcl wren].include?(t) # already have these

exercise = create(:practice_exercise, track: tracks[t], slug: e[:slug])
create(:practice_solution, :published, user:, track: tracks[t], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
end
assert badge.award_to?(user.reload), "48 golds qualifies"
end
end
92 changes: 92 additions & 0 deletions test/models/badges/paavo_nurmi_badge_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
require "test_helper"

class Badge::PaavoNurmiBadgeTest < ActiveSupport::TestCase
test "attributes" do
badge = create :paavo_nurmi_badge
assert_equal "Paavo Nurmi", badge.name
assert_equal :ultimate, badge.rarity
assert_equal :'paavo-nurmi', badge.icon
assert_equal 'Earned 48 gold or silver medals in the #48in24 challenge', badge.description
assert badge.send_email_on_acquisition?
assert_nil badge.notification_key
end

test "award_to?" do
badge = create :paavo_nurmi_badge
exercises = User::Challenges::FeaturedExercisesProgress48In24::EXERCISES
week_1 = exercises.find { |e| e[:week] == 1 }
tracks = exercises.map { |e| e[:featured_tracks] }.flatten.uniq.
map { |track_slug| [track_slug.to_sym, create(:track, slug: track_slug)] }.
to_h
user = create :user

# No solutions
refute badge.award_to?(user.reload)

create :user_challenge, user:, challenge_id: '48in24'
refute badge.award_to?(user.reload)

# One bronze all year
exercise = create(:practice_exercise, track: tracks[:csharp], slug: week_1[:slug])
create(:practice_solution, :published, user:, track: tracks[:csharp], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
refute badge.award_to?(user.reload)

# One silver medal: csharp+tcl+wren are never the featured tracks for an exercise
%i[tcl wren].each do |track_slug|
exercise = create(:practice_exercise, track: tracks[track_slug], slug: week_1[:slug])
create(:practice_solution, :published, user:, track: tracks[track_slug], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
refute badge.award_to?(user.reload)

# One gold medal
week_1[:featured_tracks].each do |track_slug|
exercise = create(:practice_exercise, track: tracks[track_slug.to_sym], slug: week_1[:slug])
create(:practice_solution, :published, user:, track: tracks[track_slug.to_sym], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
refute badge.award_to?(user.reload)

# in addition to week 1 gold, add 46 bronze medals: 47 medals does not qualify
exercises.reject { |e| e[:week] == 1 || e[:week] == 48 }.each do |e|
exercise = create(:practice_exercise, track: tracks[:csharp], slug: e[:slug])
create(:practice_solution, :published, user:, track: tracks[:csharp], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
refute badge.award_to?(user.reload)

# add week 48 solution, not in 2024
week_48 = exercises.find { |e| e[:week] == 48 }
exercise = create(:practice_exercise, track: tracks[:csharp], slug: week_48[:slug])
solution = create(:practice_solution, :published, user:, track: tracks[:csharp], exercise:,
completed_at: Time.utc(2023, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
refute badge.award_to?(user.reload)

# change completion date to 2024, still does not qualify
solution.update(completed_at: Time.utc(2024, 1, 1, 0, 0, 0))
refute badge.award_to?(user.reload)

# 48 gold or silver medals
exercises.reject { |e| e[:week] == 1 }.each do |e|
%i[tcl wren].each do |t|
exercise = create(:practice_exercise, track: tracks[t], slug: e[:slug])
create(:practice_solution, :published, user:, track: tracks[t], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
end
assert badge.award_to?(user.reload)

# 48 gold medals
exercises.reject { |e| e[:week] == 1 }.each do |e|
e[:featured_tracks].map(&:to_sym).each do |t|
next if %i[csharp tcl wren].include?(t) # already have these

exercise = create(:practice_exercise, track: tracks[t], slug: e[:slug])
create(:practice_solution, :published, user:, track: tracks[t], exercise:,
completed_at: Time.utc(2024, SecureRandom.rand(1..12), SecureRandom.rand(1..28)))
end
end
assert badge.award_to?(user.reload)
end
end
Loading
Loading