From 97da614fb56af6ffd6e755957527703d5d6bdb94 Mon Sep 17 00:00:00 2001 From: Chris Zetter <253059100+zetter-rpf@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:30:57 +0100 Subject: [PATCH 1/2] Setup tasks for rspec Note that you will need to have asdf on your non-interactive shell path for this to work. If you see a bunder not installed message this is likely the cause. You can fix this by making sure asdf is added to your path in your zprofile instead of your zshrc. I've added the rspec bin stub as part of this as it's quicker than typing bundle exec and simpler than using it in the tasks.json --- .vscode/tasks.json | 41 +++++++++++++++++++++++++++++++++++++++++ bin/rspec | 16 ++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .vscode/tasks.json create mode 100755 bin/rspec diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..1dc16663e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run test file (RSpec)", + "type": "shell", + "command": "./bin/rspec", + "args": [ + "${file}" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "test", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [] + }, + { + "label": "Run test at current line (RSpec)", + "type": "shell", + "command": "./bin/rspec", + "args": [ + "${file}:${lineNumber}" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "test", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [] + } + ] +} diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 000000000..93e191c2f --- /dev/null +++ b/bin/rspec @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") From 0a46ba42493c7e3081652321ffaf464b197eb2bc Mon Sep 17 00:00:00 2001 From: Chris Zetter <253059100+zetter-rpf@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:05:47 +0100 Subject: [PATCH 2/2] Avoid N+1 queries when loading submitted counts While we have bullet, but it wasn't failing because there wasn't enough test data set up. Since the extra includes is only used by teachers, I've had to change the controller action around bit. Newer version of rails have a 'strict_loading' mode which can help prevent N+1 queries, however I don't want to introduce it until after we upgrade to rails 8 and can configure the 'strict_loading_mode' to only n+1 queries otherwise there is a lot of noise from single queries[1]. [1] - https://thoughtbot.com/blog/strict-loading-in-rails-8-a-railsy-way-to-avoid-n-1-queries --- app/controllers/api/lessons_controller.rb | 6 ++++-- app/models/lesson.rb | 2 +- app/models/project.rb | 1 + spec/features/lesson/listing_lessons_spec.rb | 12 +++++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/lessons_controller.rb b/app/controllers/api/lessons_controller.rb index ebcf702db..06876cf56 100644 --- a/app/controllers/api/lessons_controller.rb +++ b/app/controllers/api/lessons_controller.rb @@ -11,13 +11,15 @@ class LessonsController < ApiController def index accessible_lessons = filtered_lessons_scope .accessible_by(current_ability) - .includes(project: :remixes) - @lessons_with_users = accessible_lessons.with_users if current_user&.school_teacher?(school) || current_user&.school_owner?(school) + accessible_lessons = accessible_lessons.includes(project: { remixes: %i[submitted_school_project] }) + @lessons_with_users = accessible_lessons.with_users render :teacher_index, formats: [:json], status: :ok else remixes = user_remixes(accessible_lessons) + accessible_lessons = accessible_lessons.includes(project: :remixes) + @lessons_with_users = accessible_lessons.with_users @lessons_with_users_and_remixes = @lessons_with_users.zip(remixes) render :student_index, formats: [:json], status: :ok end diff --git a/app/models/lesson.rb b/app/models/lesson.rb index e3b7a433d..69e7c02a4 100644 --- a/app/models/lesson.rb +++ b/app/models/lesson.rb @@ -35,7 +35,7 @@ def with_user def submitted_count return 0 unless project - project.remixes.count { |remix| remix.school_project&.submitted? } + project.remixes.count(&:submitted_school_project) end private diff --git a/app/models/project.rb b/app/models/project.rb index db745bee2..fd839f3a3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -19,6 +19,7 @@ module Types has_many_attached :videos has_many_attached :audio has_one :school_project, dependent: :destroy + has_one :submitted_school_project, -> { in_state(:submitted) }, class_name: 'SchoolProject', dependent: :destroy, inverse_of: :project accepts_nested_attributes_for :components accepts_nested_attributes_for :scratch_component diff --git a/spec/features/lesson/listing_lessons_spec.rb b/spec/features/lesson/listing_lessons_spec.rb index 4128991f9..85c7c4c48 100644 --- a/spec/features/lesson/listing_lessons_spec.rb +++ b/spec/features/lesson/listing_lessons_spec.rb @@ -86,14 +86,20 @@ end it 'includes the submitted_count for each lesson' do + student_2 = create(:student, school:) + create(:class_student, school_class:, student_id: student_2.id) + lesson.update!(school_class_id: school_class.id) - remix = create(:project, school:, remixed_from_id: lesson.project.id, user_id: student.id) - remix.school_project.transition_status_to!(:submitted, student.id) + + [student, student_2].each do |student| + remix = create(:project, school:, remixed_from_id: lesson.project.id, user_id: student.id) + remix.school_project.transition_status_to!(:submitted, student.id) + end get("/api/lessons?school_class_id=#{school_class.id}", headers:) data = JSON.parse(response.body, symbolize_names: true) - expect(data.first[:submitted_count]).to eq(1) + expect(data.first[:submitted_count]).to eq(2) end context 'when filtering by project_identifier' do