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

Add track trophies! #5749

Merged
merged 95 commits into from
Aug 18, 2023
Merged

Add track trophies! #5749

merged 95 commits into from
Aug 18, 2023

Conversation

iHiD
Copy link
Member

@iHiD iHiD commented Jul 19, 2023

Modal here probably needs some tweaking cc @iHiD

Screen.Recording.2023-08-15.at.13.27.48.mov

Error revealing:

Screen.Recording.2023-08-15.at.13.22.17.mov

@iHiD iHiD force-pushed the simplify-track branch from 7ed75db to 06f4c6c Compare July 26, 2023 17:15
@ErikSchierboom ErikSchierboom force-pushed the simplify-track branch 2 times, most recently from 41ecf56 to 5296b29 Compare August 4, 2023 08:25
@@ -53,6 +53,7 @@ def call
)

Git::SyncTrackDocs.(track, force_sync:)
Track::Trophy::ReseedVariableTrophies.()
Copy link
Member

@ErikSchierboom ErikSchierboom Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command will reseed any trophies, which allowed tracks depend on some state of the track.

For example, the trophy to complete learning mode only applies to tracks with learning mode enabled, but that value might change.
What we do here is after syncing the track (meaning: its state might have changed), we'll reseed the variable trophies (like the aforementioned one), updating the track slugs dynamically.

Comment on lines 11 to 12
Track::Trophies::Shared::CompletedFiveHardExercisesTrophy,
Track::Trophies::Shared::CompletedLearningModeTrophy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both these trophies are dependent on the state of the track

Comment on lines 16 to 29
def self.worth_queuing?(track, category, slug, context:)
# General trophies apply to _all_ tracks, so there won't be any track filtering.
# If the context is also empty, there is no possibility of the trophy not being
# worth queuing
return true if category == :general && context.empty?

args = { track: }
args[context.class.base_class.name.demodulize.underscore] = context if context.present?

badge = "Track::Trophies::#{category.to_s.camelize}::#{slug.to_s.camelize}Trophy".safe_constantize
badge.worth_queuing?(**args.symbolize_keys)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-used and tweaked the logic from AwardBadgeJob a bit

@@ -3,6 +3,6 @@
#page-track-show
= render ViewComponents::Track::Header.new(@track, :overview)

= render "tracks/show/summary_article", track: @track, user_track: @user_track, exercise_statuses: @exercise_statuses, recent_solutions: @recent_solutions, sample_learnt_concepts: @sample_learnt_concepts, sample_mastered_concepts: @sample_mastered_concepts, last_8_weeks_counts: @last_8_weeks_counts
= render "tracks/show/summary_article", track: @track, user_track: @user_track, exercise_statuses: @exercise_statuses, recent_solutions: @recent_solutions, sample_learnt_concepts: @sample_learnt_concepts, sample_mastered_concepts: @sample_mastered_concepts, last_8_weeks_counts: @last_8_weeks_counts, forum_threads: @forum_threads, trophies: @track.trophies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were some linting errors that this fixes

app/jobs/award_trophy_job.rb Outdated Show resolved Hide resolved
db/schema.rb Outdated Show resolved Hide resolved
db/schema.rb Outdated Show resolved Hide resolved
@ErikSchierboom ErikSchierboom marked this pull request as ready for review August 15, 2023 13:19
box-shadow: 0px 4px 8px 0px rgba(var(--shadowColorMain), 0.2);
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dem4ron Can you rename both of these animations to be trophy-... please.

<div className="c-alert--danger">Failed to reveal trophy</div>
)}
</button>
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic here is a bit different to what is in my original code.

There are three states:

  • revealed: The trophy gets the acquired class. It's a button. Clicking on it opens the modal
  • unrevealed: The trophy gets the revealable class. It's a button. Clicking on it opens the modal and reveals it.
  • not_earned: The trophy gets the not-acquired class. It's a div. Hovering opens a tooltip.

Co-authored-by: Jeremy Walker <jez.walker@gmail.com>
@ErikSchierboom ErikSchierboom force-pushed the simplify-track branch 2 times, most recently from 974b1b7 to 919dc81 Compare August 16, 2023 14:04
@ErikSchierboom
Copy link
Member

ErikSchierboom commented Aug 17, 2023

We can backport the trophies using:

Track::Trophy::Reseed.()

trophies = %i[
  completed_all_exercises 
  completed_fifty_percent_of_exercises
  completed_five_hard_exercises
  completed_learning_mode
  completed_twenty_exercises
  iterated_twenty_exercises
].map { |slug| Track::Trophy.lookup!(slug) }

Track.active.find_each do |track|
  File.write('/tmp/trophies.txt', "#{Time.now} [#{track.slug}] start\n", mode: 'a+')

  track_trophies = trophies.select { |trophy| trophy.enabled_for_track?(track) }

  num_processed = 0

  User.where(id: UserTrack.select(:user_id).where(track:)).find_each do |user|
    next if user.id == User::GHOST_USER_ID

    track_trophies.each do |trophy|
      next unless trophy.award?(user, track)

      begin
        UserTrack::AcquiredTrophy.create(user:, track:, trophy:)
      rescue ActiveRecord::RecordNotUnique
        # Ignore existing trophies
      end
    end

    num_processed += 1

    File.write('/tmp/trophies.txt', "#{Time.now} [#{track.slug}] processed #{num_processed}/#{track.num_students} students\n", mode: 'a+') if num_processed % 1000 == 0
  end

  File.write('/tmp/trophies.txt', "#{Time.now} [#{track.slug}] done\n\n", mode: 'a+')
end

mentored_trophy = Track::Trophy.lookup!(:mentored)

Mentor::Discussion.finished.includes(request: [:student, :track]).find_each do |discussion|
  user = discussion.request.student
  track = discussion.request.track

  begin
    UserTrack::AcquiredTrophy.create(user:, track:, trophy: mentored_trophy)
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

@iHiD iHiD merged commit b16c207 into main Aug 18, 2023
@iHiD iHiD deleted the simplify-track branch August 18, 2023 21:45
@ErikSchierboom
Copy link
Member

ErikSchierboom commented Aug 22, 2023

Backport progress:

  • completed_all_exercises
  • completed_fifty_percent_of_exercises
  • completed_five_hard_exercises
  • completed_learning_mode
  • completed_twenty_exercises
  • iterated_twenty_exercises
  • mentored

@ErikSchierboom
Copy link
Member

Backporting mentored trophy:

Mentor::Discussion.finished.includes(request: [:student, :track]).find_each do |discussion|
  user = discussion.request.student
  track = discussion.request.track

  begin
    UserTrack::AcquiredTrophy.create(user:, track:, trophy: mentored_trophy)
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

@ErikSchierboom
Copy link
Member

ErikSchierboom commented Aug 22, 2023

Backporting iterated_twenty_exercises trophy:

created_at = Time.current
trophy = Track::Trophy.lookup!(:iterated_twenty_exercises)

Track.active.find_each do |track|
    user_ids = Solution.joins(:exercise).
        where(exercise: { track: }).
        where('num_iterations >= 2').
        group(:user_id).
        count.
        filter_map { |user_id, count| user_id if count >= 20 }

    User.where(id: user_ids).find_each do |user|
        UserTrack::AcquiredTrophy.create(user:, track:, trophy:, created_at:)
    rescue ActiveRecord::RecordNotUnique
      # Ignore existing trophies
    end
end

@iHiD
Copy link
Member Author

iHiD commented Aug 22, 2023

Backporting completed_twenty_exercises:

created_at = Time.current
trophy = Track::Trophy.lookup!(:completed_twenty_exercises)

Track.active.find_each do |track|
  user_ids = Solution.where.not(completed_at: nil).joins(:exercise).
    where(exercise: { track: }).
    group(:user_id).
    count.
    filter_map { |user_id, count| user_id if count >= 20 }

  User.where(id: user_ids).find_each do |user|
    UserTrack::AcquiredTrophy.create(user:, track:, trophy:. created_at:) 
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

@iHiD
Copy link
Member Author

iHiD commented Aug 22, 2023

Backporting completed_fifty_percent_of_exercises :

created_at = Time.current
trophy = Track::Trophy.lookup!(:completed_fifty_percent_of_exercises)

Track.active.find_each do |track|
  halfway_there = (track.exercises.where(status: %i[beta active]).count / 2).ceil
  user_ids = Solution.where.not(completed_at: nil).joins(:exercise).
    where(exercise: { track: }).
    group(:user_id).
    count.
    filter_map { |user_id, count| user_id if count >= halfway_there }

  User.where(id: user_ids).find_each do |user|
    UserTrack::AcquiredTrophy.create(user:, track:, trophy:, created_at:) 
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

@iHiD
Copy link
Member Author

iHiD commented Aug 22, 2023

Backporting completed_all_exercises:

created_at = Time.current
trophy = Track::Trophy.lookup!(:completed_all_exercises)

Track.active.find_each do |track|
  num_exercises = track.exercises.where(status: %i[beta active]).count
  user_ids = Solution.where.not(completed_at: nil).joins(:exercise).
    where(exercise: { track: }).
    group(:user_id).
    count.
    filter_map { |user_id, count| user_id if count >= num_exercises }

  User.where(id: user_ids).find_each do |user|
    UserTrack::AcquiredTrophy.create(user:, track:, trophy:, created_at:) 
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

@iHiD
Copy link
Member Author

iHiD commented Aug 22, 2023

Backporting completed_learning_mode:

created_at = Time.current
trophy = Track::Trophy.lookup!(:completed_learning_mode)

Track.active.find_each do |track|
  next unless track.course?

  num_concept_exercises = track.concept_exercises.where(status: %i[beta active]).count
  exercise_ids = track.concept_exercises.where(status: %i[beta active]).pluck(:id)
  user_ids = ConceptSolution.where.not(completed_at: nil).joins(:exercise).
    where(exercise: exercise_ids).
    group(:user_id).
    count.
    filter_map { |user_id, count| user_id if count >= num_concept_exercises }

  user_ids.each do |user_id|
    user_track = UserTrack.find_by(track_id: track.id, user_id: user_id)
    next unless user_track
    next unless user_track.num_completed_concept_exercises >= num_concept_exercises
    
    UserTrack::AcquiredTrophy.create(user: user_track.user, track:, trophy:, created_at:) 
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

@ErikSchierboom
Copy link
Member

Backport complete_five_hard_exercises:

created_at = Time.current
trophy = Track::Trophy.lookup!(:completed_five_hard_exercises)

Track.active.find_each do |track|
  exercise_ids = track.exercises.where('difficulty >= 8').pluck(:id)
  user_ids = Solution.where.not(completed_at: nil).joins(:exercise).
    where(exercise: exercise_ids).
    group(:user_id).
    count.
    filter_map { |user_id, count| user_id if count >= 5 }

  User.where(id: user_ids).find_each do |user|
    UserTrack::AcquiredTrophy.create(user:, track:, trophy:. created_at:) 
  rescue ActiveRecord::RecordNotUnique
    # Ignore existing trophies
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants