Skip to content

Commit 9e04668

Browse files
authored
Add remixes endpoint (#410)
## Status - Closes _add issue numbers or delete_ - Related to _add issue numbers or delete_ ## Points for consideration: - Security - Performance ## What's changed? _Description of what's been done - bullets are often best_ ## Steps to perform after deploying to production _If the production environment requires any extra work after this PR has been deployed detail it here. This could be running a Rake task, a migration, or upgrading a Gem. That kind of thing._
1 parent 1afa32d commit 9e04668

File tree

6 files changed

+118
-31
lines changed

6 files changed

+118
-31
lines changed

app/controllers/api/projects/remixes_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ module Api
44
module Projects
55
class RemixesController < ApiController
66
before_action :authorize_user
7+
load_and_authorize_resource :school, only: :index
8+
9+
def index
10+
projects = Project.where(remixed_from_id: project.id).accessible_by(current_ability)
11+
@projects_with_users = projects.with_users(@current_user)
12+
render index: @projects_with_users, formats: [:json]
13+
end
714

815
def show
916
@project = Project.find_by!(remixed_from_id: project.id, user_id: current_user&.id)

app/models/project.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@ class Project < ApplicationRecord
2121

2222
scope :internal_projects, -> { where(user_id: nil) }
2323

24-
def self.users
25-
User.from_userinfo(ids: pluck(:user_id))
24+
def self.users(current_user)
25+
school = School.find_by(id: pluck(:school_id))
26+
SchoolStudent::List.call(school:, token: current_user.token, student_ids: pluck(:user_id))[:school_students] || []
2627
end
2728

28-
def self.with_users
29-
by_id = users.index_by(&:id)
29+
def self.with_users(current_user)
30+
by_id = users(current_user).index_by(&:id)
3031
all.map { |instance| [instance, by_id[instance.user_id]] }
3132
end
3233

33-
def with_user
34-
[self, User.from_userinfo(ids: user_id).first]
34+
def with_user(current_user)
35+
school = School.find_by(id: :school_id)
36+
students = SchoolStudent::List.call(school:, token: current_user.token,
37+
student_ids: [:user_id])[:school_students] || []
38+
[self, students.first]
3539
end
3640

3741
# Work around a CanCanCan issue with accepts_nested_attributes_for.
@@ -40,6 +44,11 @@ def components=(array)
4044
super(array.map { |o| o.is_a?(Hash) ? Component.new(o) : o })
4145
end
4246

47+
def last_edited_at
48+
# datetime that the project or one of its components was last updated
49+
[updated_at, components.maximum(:updated_at)].compact.max
50+
end
51+
4352
private
4453

4554
def check_unique_not_null
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
json.array!(@projects_with_users) do |project, user|
4+
json.call(
5+
project,
6+
:identifier,
7+
:project_type,
8+
:name,
9+
:user_id,
10+
:updated_at,
11+
:last_edited_at
12+
)
13+
14+
json.user_name(user&.name)
15+
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
resources :projects, only: %i[index show update destroy create] do
3636
resource :remix, only: %i[show create], controller: 'projects/remixes'
37+
resources :remixes, only: %i[index], controller: 'projects/remixes'
3738
resource :images, only: %i[show create], controller: 'projects/images'
3839
end
3940

spec/models/project_spec.rb

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -127,84 +127,117 @@
127127
end
128128

129129
describe '.users' do
130-
it 'returns User instances for the current scope' do
131-
student = create(:student, school:, name: 'School Student')
132-
stub_user_info_api_for(student)
133-
create(:project, user_id: student.id)
130+
let(:student) { create(:student, school:, name: 'School Student') }
131+
let(:teacher) { create(:teacher, school:) }
132+
133+
let(:student_attributes) do
134+
[{ id: student.id, name: student.name, username: student.username }]
135+
end
134136

135-
user = described_class.all.users.first
137+
before do
138+
stub_profile_api_list_school_students(school:, student_attributes:)
139+
end
140+
141+
it 'returns User instances for the current scope' do
142+
create(:project, user_id: student.id, school_id: school.id)
143+
user = described_class.all.users(teacher).first
136144
expect(user.name).to eq('School Student')
137145
end
138146

139147
it 'ignores members where no profile account exists' do
140148
user_id = SecureRandom.uuid
141-
stub_user_info_api_for_unknown_users(user_id:)
142149
create(:project, user_id:)
143150

144-
user = described_class.all.users.first
151+
user = described_class.all.users(teacher).first
145152
expect(user).to be_nil
146153
end
147154

148155
it 'ignores members not included in the current scope' do
149156
create(:project)
150157

151-
user = described_class.none.users.first
158+
user = described_class.none.users(teacher).first
152159
expect(user).to be_nil
153160
end
154161
end
155162

156163
describe '.with_users' do
157-
# rubocop:disable RSpec/ExampleLength
164+
let(:student) { create(:student, school:) }
165+
let(:teacher) { create(:teacher, school:) }
166+
167+
let(:student_attributes) do
168+
[{ id: student.id, name: student.name, username: student.username }]
169+
end
170+
171+
before do
172+
stub_profile_api_list_school_students(school:, student_attributes:)
173+
end
174+
158175
it 'returns an array of class members paired with their User instance' do
159-
student = create(:student, school:)
160-
stub_user_info_api_for(student)
161176
project = create(:project, user_id: student.id)
162177

163-
pair = described_class.all.with_users.first
164-
user = described_class.all.users.first
178+
pair = described_class.all.with_users(teacher).first
179+
user = described_class.all.users(teacher).first
165180

166181
expect(pair).to eq([project, user])
167182
end
168-
# rubocop:enable RSpec/ExampleLength
169183

170184
it 'returns nil values for members where no profile account exists' do
171185
user_id = SecureRandom.uuid
172-
stub_user_info_api_for_unknown_users(user_id:)
173186
project = create(:project, user_id:)
174187

175-
pair = described_class.all.with_users.first
188+
pair = described_class.all.with_users(teacher).first
176189
expect(pair).to eq([project, nil])
177190
end
178191

179192
it 'ignores members not included in the current scope' do
180193
create(:project)
181194

182-
pair = described_class.none.with_users.first
195+
pair = described_class.none.with_users(teacher).first
183196
expect(pair).to be_nil
184197
end
185198
end
186199

187200
describe '#with_user' do
188-
# rubocop:disable RSpec/ExampleLength
201+
let(:student) { create(:student, school:) }
202+
let(:teacher) { create(:teacher, school:) }
203+
204+
let(:student_attributes) do
205+
[{ id: student.id, name: student.name, username: student.username }]
206+
end
207+
208+
before do
209+
stub_profile_api_list_school_students(school:, student_attributes:)
210+
end
211+
189212
it 'returns the class member paired with their User instance' do
190-
student = create(:student, school:)
191-
stub_user_info_api_for(student)
192213
project = create(:project, user_id: student.id)
193214

194-
pair = project.with_user
195-
user = described_class.all.users.first
215+
pair = project.with_user(teacher)
216+
user = described_class.all.users(teacher).first
196217

197218
expect(pair).to eq([project, user])
198219
end
199-
# rubocop:enable RSpec/ExampleLength
200220

201221
it 'returns a nil value if the member has no profile account' do
202222
user_id = SecureRandom.uuid
203-
stub_user_info_api_for_unknown_users(user_id:)
204223
project = create(:project, user_id:)
205224

206-
pair = project.with_user
225+
pair = project.with_user(teacher)
207226
expect(pair).to eq([project, nil])
208227
end
209228
end
229+
230+
describe '#last_edited_at' do
231+
let(:project) { create(:project, updated_at: 1.day.ago) }
232+
let(:component) { create(:component, project:, updated_at: 2.days.ago) }
233+
234+
it 'returns the project updated_at if most recent' do
235+
expect(project.last_edited_at).to eq(project.updated_at)
236+
end
237+
238+
it 'returns the latest component updated_at if most recent' do
239+
latest_component = create(:component, project:, updated_at: 1.hour.ago)
240+
expect(project.last_edited_at).to eq(latest_component.updated_at)
241+
end
242+
end
210243
end

spec/requests/projects/remix_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@
3030
authenticated_in_hydra_as(owner)
3131
end
3232

33+
describe '#index' do
34+
before do
35+
create_list(:project, 2, remixed_from_id: original_project.id, user_id: authenticated_user.id)
36+
end
37+
38+
it 'returns success response' do
39+
get("/api/projects/#{original_project.identifier}/remixes", headers:)
40+
expect(response).to have_http_status(:ok)
41+
end
42+
43+
it 'returns the list of projects' do
44+
get("/api/projects/#{original_project.identifier}/remixes", headers:)
45+
expect(response.parsed_body.length).to eq(2)
46+
end
47+
48+
it 'returns 404 response if invalid project' do
49+
get('/api/projects/no-such-project/remixes', headers:)
50+
51+
expect(response).to have_http_status(:not_found)
52+
end
53+
end
54+
3355
describe '#show' do
3456
before do
3557
create(:project, remixed_from_id: original_project.id, user_id: authenticated_user.id)

0 commit comments

Comments
 (0)