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 Bootcamp #7214

Merged
merged 39 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7677067
Add migrations
iHiD Dec 30, 2024
9301572
Add new models and factories
iHiD Dec 30, 2024
0039082
Add commands
iHiD Dec 30, 2024
6445180
Add CSS and content
iHiD Dec 30, 2024
d37ee02
WIP
iHiD Dec 30, 2024
d06b004
Fix shizzle
iHiD Dec 30, 2024
aacf31b
Rename stub
iHiD Dec 30, 2024
457933c
Improve CSS
iHiD Dec 31, 2024
2b03dad
Copy over tests
iHiD Jan 1, 2025
17279d6
Fix some tests
iHiD Jan 1, 2025
7f41e18
Fix widget and highlighted line positioning (#7219)
dem4ron Jan 2, 2025
4667335
Add example solution for manual_solve (#7220)
dem4ron Jan 2, 2025
782efb9
Move over API endpoints (#7227)
iHiD Jan 3, 2025
f46a58b
Remove typewriter wrapper after finished typing instructions (#7228)
dem4ron Jan 3, 2025
447ef3e
Adjust endpoints (#7229)
dem4ron Jan 3, 2025
ee81d35
Update maze exercise config (#7230)
dem4ron Jan 3, 2025
2dbe82e
Add modal css (#7231)
dem4ron Jan 3, 2025
7f2178c
Rename description html to error html everywhere (#7232)
dem4ron Jan 3, 2025
e4e7b08
Disable infowidget on play (#7233)
dem4ron Jan 3, 2025
8c9f521
Change drawing animation duration (#7234)
dem4ron Jan 3, 2025
e5737d7
Add correct links to anchors (#7235)
dem4ron Jan 3, 2025
84b136a
Add reset button (#7236)
dem4ron Jan 6, 2025
6ca5f29
Use config title to save editor value (#7240)
dem4ron Jan 6, 2025
c7885bb
Import jikiscript (#7241)
dem4ron Jan 6, 2025
a2cc93a
Hide scrollbar on `edit` page (#7242)
dem4ron Jan 6, 2025
0760ac8
Add change keyword (#7243)
iHiD Jan 7, 2025
82077b1
Preview all scenarios, unify LHS styling (#7249)
dem4ron Jan 8, 2025
f2586a1
Update info widget disabling logic, hide play button if no animation …
dem4ron Jan 8, 2025
62931cf
Cleanup DOM on each new test run (#7251)
dem4ron Jan 8, 2025
1450a5a
Use latest code (#7253)
dem4ron Jan 8, 2025
4d827a7
Use SVGs for drawing (#7247)
iHiD Jan 8, 2025
97e236e
Add drawing views (#7244)
iHiD Jan 8, 2025
5c14ddd
Remove DrawTest (#7257)
dem4ron Jan 8, 2025
cbf70fb
Make tests pass (#7255)
dem4ron Jan 8, 2025
8360c7a
Add jumbled house (#7254)
iHiD Jan 8, 2025
d994337
Add repeat delay for animations (#7222)
iHiD Jan 8, 2025
8cce1c8
Merge branch 'main' into bootcamp-ui
iHiD Jan 8, 2025
463c0c3
Bootcamp holding (#7256)
iHiD Jan 8, 2025
a174512
Update db/migrate/20241119025227_create_bootcamp_user_projects.rb
iHiD Jan 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
35,478 changes: 35,478 additions & 0 deletions app/animations/confetti.json

Large diffs are not rendered by default.

2,495 changes: 2,495 additions & 0 deletions app/animations/finish-lesson-modal-top.json

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions app/commands/bootcamp/select_next_exercise.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Bootcamp::SelectNextExercise
include Mandate

initialize_with :user, project: nil

def call
return next_user_project_exercise if next_user_project_exercise

Bootcamp::Exercise.unlocked.where.not(project: user.bootcamp_projects).
where.not(id: completed_exercise_ids).first
end

def user_project
if project
user_project = Bootcamp::UserProject.for!(user, project)
return user_project if user_project.available?
end

user.bootcamp_user_projects.where(status: :available).first
end

memoize
def next_user_project_exercise = user_project&.next_exercise

def completed_exercise_ids
user.bootcamp_solutions.where.not(completed_at: nil).select(:exercise_id)
end
end
23 changes: 23 additions & 0 deletions app/commands/bootcamp/solution/complete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Bootcamp::Solution::Complete
include Mandate

initialize_with :solution

def call
# It's essential that both of these lines are called
# inline to ensure next exercise selection is correct
solution.update!(completed_at: Time.current)
Bootcamp::UserProject::UpdateStatus.(user_project)

schedule_deferred_jobs!
end

private
def schedule_deferred_jobs!
Bootcamp::UpdateUserLevel.defer(user)
end

def user_project = Bootcamp::UserProject.for!(user, project)

delegate :user, :project, to: :solution
end
43 changes: 43 additions & 0 deletions app/commands/bootcamp/solution/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class Bootcamp::Solution::Create
include Mandate

initialize_with :user, :exercise

def call
guard!

begin
Bootcamp::Solution.create!(
user:,
exercise:,
code:
)
rescue ActiveRecord::RecordNotUnique
Bootcamp::Solution.find_by!(
user:,
exercise:
)
end
end

private
def guard!
raise ExerciseLockedError unless user_project.exercise_available?(exercise)
end

def code
exercise.stub.gsub(Regexp.new("{{EXERCISE_([-a-z0-9]+)}}")) do
previous_solutions[::Regexp.last_match(1)].code
end
end

# This is used for the code interpolation, only if actually required.
memoize
def previous_solutions
user_project.solutions.index_by { |s| s.exercise.slug }
end

def user_project
Bootcamp::UserProject.find_by!(user:, project: exercise.project)
end
end
27 changes: 27 additions & 0 deletions app/commands/bootcamp/submission/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Bootcamp::Submission::Create
include Mandate

initialize_with :solution, :code, :test_results, :readonly_ranges

def call
create_submission.tap do |submission|
fire_events!(submission)
end
end

private
def create_submission
Bootcamp::Submission.create!(
solution:,
code:,
test_results: test_results.to_h,
readonly_ranges: readonly_ranges || []
)
end

def fire_events!(_submission)
nil # TODO: Implement this
# if submission.passed?
# end
end
end
30 changes: 30 additions & 0 deletions app/commands/bootcamp/update_user_level.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class Bootcamp::UpdateUserLevel
include Mandate

initialize_with :user

def call
max = 0
exercise_ids_by_level_idx.each do |level_idx, exercise_ids|
next if level_idx < max
break unless solved_exercise_ids_by_level_idx[level_idx] == exercise_ids

max = level_idx
end
user.bootcamp_data.update!(level_idx: max)
end

memoize
def exercise_ids_by_level_idx
Bootcamp::Exercise.pluck(:level_idx, :id).
group_by(&:first).
transform_values { |v| v.map(&:last) }.
sort.to_h
end

def solved_exercise_ids_by_level_idx
user.bootcamp_solutions.completed.joins(:exercise).pluck(:level_idx, :exercise_id).
group_by(&:first).
transform_values { |v| v.map(&:last) }
end
end
19 changes: 19 additions & 0 deletions app/commands/bootcamp/user_project/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Bootcamp::UserProject::Create
include Mandate

initialize_with :user, :project

def call
Bootcamp::Project.find_each do |_project|
find_or_create.tap do |up|
Bootcamp::UserProject::UpdateStatus.(up)
end
end
end

def find_or_create
Bootcamp::UserProject.create!(user:, project:)
rescue StandardError
Bootcamp::UserProject.find_by!(user:, project:)
end
end
11 changes: 11 additions & 0 deletions app/commands/bootcamp/user_project/create_all.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Bootcamp::UserProject::CreateAll
include Mandate

initialize_with :user

def call
Bootcamp::Project.find_each do |project|
Bootcamp::UserProject::Create.defer(user, project)
end
end
end
29 changes: 29 additions & 0 deletions app/commands/bootcamp/user_project/update_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class Bootcamp::UserProject::UpdateStatus
include Mandate

initialize_with :user_project

def call
user_project.update!(status:)
end

def status
return :available if user_project.solutions.any?(&:in_progress?)

num_unlocked_exercises = user_project.unlocked_exercises.count
num_solutions = user_project.solutions.count

# If there are no unlocked exercises, the project is locked
return :locked if num_unlocked_exercises.zero?

# If all solutions have an exercise and they're all complete, the project is complete
return :completed if num_solutions == num_unlocked_exercises &&
user_project.solutions.all?(&:completed?)

# If we have more exercises than solutions, then there's a new solution
# that can be created if the user wants to.
return :available if num_unlocked_exercises > num_solutions

:locked
end
end
1 change: 1 addition & 0 deletions app/commands/document/sync_all_to_search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def call
end

Exercism.opensearch_client.bulk(body:)
Exercism::TOUCHED_OPENSEARCH_INDEXES << Document::OPENSEARCH_INDEX if Rails.env.test?
end
end

Expand Down
1 change: 1 addition & 0 deletions app/commands/document/sync_to_search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ def call
id: doc.id,
body: Document::CreateSearchIndexDocument.(doc)
)
Exercism::TOUCHED_OPENSEARCH_INDEXES << Document::OPENSEARCH_INDEX if Rails.env.test?
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def create_document!
id: representation.id,
body: Exercise::Representation::CreateSearchIndexDocument.(representation)
)
Exercism::TOUCHED_OPENSEARCH_INDEXES << Exercise::Representation::OPENSEARCH_INDEX if Rails.env.test?
end

def delete_document!
Expand Down
1 change: 1 addition & 0 deletions app/commands/solution/sync_all_to_search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def call
end

Exercism.opensearch_client.bulk(body:)
Exercism::TOUCHED_OPENSEARCH_INDEXES << Solution::OPENSEARCH_INDEX if Rails.env.test?
end
end

Expand Down
1 change: 1 addition & 0 deletions app/commands/solution/sync_to_search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def create_document!
id: solution.id,
body: Solution::CreateSearchIndexDocument.(solution)
)
Exercism::TOUCHED_OPENSEARCH_INDEXES << Solution::OPENSEARCH_INDEX if Rails.env.test?
end

def delete_document!
Expand Down
12 changes: 4 additions & 8 deletions app/commands/user/bootstrap.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class User::Bootstrap
include Mandate

initialize_with :user
initialize_with :user, bootcamp_access_code: nil

def call
user.auth_tokens.create!
Expand All @@ -14,14 +14,10 @@ def call

private
def link_bootcamp_user!
ubd = User::BootcampData.find_by(email: user.email)
ubd = User::BootcampData.find_by(access_code: bootcamp_access_code) if bootcamp_access_code.present?
ubd ||= User::BootcampData.find_by(email: user.email)
return unless ubd

ubd.update!(user:)
User::Bootcamp::SubscribeToOnboardingEmails.defer(ubd)

return unless ubd.paid?

user.update!(bootcamp_attendee: true)
User::LinkWithBootcampData.(user, ubd)
end
end
33 changes: 33 additions & 0 deletions app/commands/user/link_with_bootcamp_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class User::LinkWithBootcampData
include Mandate

initialize_with :user, :bootcamp_data

def call
return unless bootcamp_data

reset_current_user!
reset_old_user!
bootcamp_data.update!(user:)
User::Bootcamp::SubscribeToOnboardingEmails.defer(bootcamp_data)

return unless bootcamp_data.paid?

user.update!(bootcamp_attendee: true)
end

memoize

def reset_current_user!
existing_data = User::BootcampData.find_by(user:)
return unless existing_data

existing_data.update!(user: nil)
end

def reset_old_user!
return unless bootcamp_data.user

bootcamp_data.user.update!(bootcamp_attendee: false)
end
end
24 changes: 24 additions & 0 deletions app/controllers/api/bootcamp/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class API::Bootcamp::BaseController < API::BaseController
# TODO: Move this to a middleware that explodes this out
#  during the actual parameter parsing.
def params
raw = request.parameters
return raw unless request.content_type == "application/json"
return raw unless raw[:_json].present?

raw.merge!(JSON.parse(raw.delete(:_json)))
raw
end

def use_project
@project = Bootcamp::Project.find_by!(slug: params[:project_slug])
end

def use_exercise
@exercise = @project.exercises.find_by!(slug: params[:exercise_slug])
end

def use_solution
@solution = current_user.bootcamp_solutions.find_by!(uuid: params[:solution_uuid])
end
end
22 changes: 22 additions & 0 deletions app/controllers/api/bootcamp/solutions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class API::Bootcamp::SolutionsController < API::Bootcamp::BaseController
before_action :use_solution

def complete
Bootcamp::Solution::Complete.(@solution)

next_exercise = Bootcamp::SelectNextExercise.(current_user, project: @solution.project)

render json: {
next_exercise: SerializeBootcampExercise.(next_exercise)
}, status: :ok
end

private
def use_solution
@solution = current_user.bootcamp_solutions.find_by!(uuid: params[:uuid])
end

def user_project
Bootcamp::UserProject.find_by!(user: solution.user, project: solution.exercise.project)
end
end
17 changes: 17 additions & 0 deletions app/controllers/api/bootcamp/submissions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class API::Bootcamp::SubmissionsController < API::Bootcamp::BaseController
before_action :use_solution

def create
submission = Bootcamp::Submission::Create.(
@solution,
params[:submission][:code],
params[:submission][:test_results].to_h,
params[:submission][:readonly_ranges]
)
render json: {
submission: {
uuid: submission.uuid
}
}, status: :created
end
end
Loading
Loading