Skip to content

Commit 5086e1e

Browse files
committed
Add Api::PublicProjectsController#update
We want to allow experience-cs admins to update "public" projects via experience-cs administrate UI [1]. When I say "public" projects, I mean those that are accessible to all users, because `Project#user_id` is set to `nil`. Public projects for Python & HTML are currently updated via the `GithubWebhooksController#github_push` [2] action and/or via the (possibly defunct) `projects:create_all` rake task [3] both of which make use of `ProjectImporter` [4]. However, these mechanisms are not suitable for our use case. This commit introduces a new `Api::PublicProjectsController#update` action which is somewhat based on `Api::ProjectsController#update`. I've tried to roughly follow the existing conventions however much I disagree with them, e.g. I've introduced a `PublicProject::Update` class in `lib/concepts/public_project/operations/update.rb` along the same lines as `Project::Update` in `lib/concepts/project/operations/update.rb`. This is just a skeletal starting point - I plan to flesh it out in subsequent commits. Note that I've had to limit the `restrict_project_type` before action to just the create action, because it relies on the `project.project_type` param which will not be accepted by the `update` action. [1]: https://github.com/RaspberryPiFoundation/experience-cs/issues/405 [2]: https://github.com/RaspberryPiFoundation/editor-api/blob/f348dfc92fdd8b19fbfd5434e7751340f33b4093/app/controllers/github_webhooks_controller.rb#L6-L8 [3]: https://github.com/RaspberryPiFoundation/editor-api/blob/f348dfc92fdd8b19fbfd5434e7751340f33b4093/lib/tasks/projects.rake#L4-L7 [4]: https://github.com/RaspberryPiFoundation/editor-api/blob/f348dfc92fdd8b19fbfd5434e7751340f33b4093/lib/project_importer.rb
1 parent 6f20092 commit 5086e1e

File tree

6 files changed

+177
-2
lines changed

6 files changed

+177
-2
lines changed

app/controllers/api/public_projects_controller.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
module Api
44
class PublicProjectsController < ApiController
55
before_action :authorize_user
6-
before_action :restrict_project_type
6+
before_action :restrict_project_type, only: %i[create]
7+
before_action :load_project, only: %i[update]
78

89
def create
910
authorize! :create, :public_project
@@ -17,12 +18,31 @@ def create
1718
end
1819
end
1920

21+
def update
22+
result = PublicProject::Update.call(project: @project, update_hash: update_params)
23+
24+
if result.success?
25+
@project = result[:project]
26+
render 'api/projects/show', formats: [:json]
27+
else
28+
render json: { error: result[:error] }, status: :unprocessable_entity
29+
end
30+
end
31+
2032
private
2133

34+
def load_project
35+
@project = Project.find_by!(identifier: params[:id])
36+
end
37+
2238
def create_params
2339
params.require(:project).permit(:identifier, :locale, :project_type, :name)
2440
end
2541

42+
def update_params
43+
params.require(:project).permit(:name)
44+
end
45+
2646
def restrict_project_type
2747
project_type = create_params[:project_type]
2848
return if project_type == Project::Types::SCRATCH

config/routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
resource :images, only: %i[show create], controller: 'projects/images'
4242
end
4343

44-
resources :public_projects, only: %i[create]
44+
resources :public_projects, only: %i[create update]
4545

4646
resource :project_errors, only: %i[create]
4747

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
class PublicProject
4+
class Update
5+
class << self
6+
def call(project:, update_hash:)
7+
response = OperationResponse.new
8+
9+
project.assign_attributes(update_hash)
10+
project.save!
11+
12+
response[:project] = project
13+
response
14+
rescue StandardError => e
15+
Sentry.capture_exception(e)
16+
response[:error] = "Error updating project: #{e}"
17+
response
18+
end
19+
end
20+
end
21+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe PublicProject::Update, type: :unit do
6+
describe '.call' do
7+
subject(:update_project) { described_class.call(project:, update_hash:) }
8+
9+
let(:identifier) { 'foo-bar-baz' }
10+
let!(:project) { create(:project, identifier:) }
11+
let(:update_hash) { { name: 'New name' } }
12+
13+
context 'with valid content' do
14+
it 'returns success' do
15+
expect(update_project.success?).to be(true)
16+
end
17+
18+
it 'returns project with identifier' do
19+
updated_project = update_project[:project]
20+
expect(updated_project.identifier).to eq(identifier)
21+
end
22+
end
23+
24+
context 'when update fails' do
25+
before do
26+
allow(project).to receive(:save!).and_raise('Some error')
27+
allow(Sentry).to receive(:capture_exception)
28+
end
29+
30+
it 'returns failure' do
31+
expect(update_project.failure?).to be(true)
32+
end
33+
34+
it 'sent the exception to Sentry' do
35+
update_project
36+
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
37+
end
38+
end
39+
end
40+
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Updating a public project', type: :request do
6+
let(:creator) { build(:user) }
7+
let(:project) { create(:project) }
8+
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
9+
let(:params) { { project: { name: 'New name' } } }
10+
11+
before do
12+
authenticated_in_hydra_as(creator)
13+
end
14+
15+
it 'responds 200 OK' do
16+
put("/api/public_projects/#{project.identifier}", headers:, params:)
17+
expect(response).to have_http_status(:success)
18+
end
19+
20+
it 'responds with the project JSON' do
21+
put("/api/public_projects/#{project.identifier}", headers:, params:)
22+
data = JSON.parse(response.body, symbolize_names: true)
23+
24+
expect(data).to include(name: 'New name')
25+
end
26+
27+
it 'responds 400 Bad Request when params are malformed' do
28+
put("/api/public_projects/#{project.identifier}", headers:, params: {})
29+
expect(response).to have_http_status(:bad_request)
30+
end
31+
32+
it 'responds 401 Unauthorized when no token is given' do
33+
put("/api/public_projects/#{project.identifier}", params:)
34+
expect(response).to have_http_status(:unauthorized)
35+
end
36+
37+
it 'responds 404 Not Found when project is not found' do
38+
put('/api/public_projects/another-identifier', headers:, params:)
39+
expect(response).to have_http_status(:not_found)
40+
end
41+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Update public project requests' do
6+
let(:project) { create(:project) }
7+
let(:creator) { build(:user) }
8+
let(:params) { { project: { name: 'New name' } } }
9+
10+
context 'when auth is correct' do
11+
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
12+
13+
context 'when updating project is successful' do
14+
before do
15+
authenticated_in_hydra_as(creator)
16+
17+
response = OperationResponse.new
18+
response[:project] = project
19+
allow(PublicProject::Update).to receive(:call).and_return(response)
20+
end
21+
22+
it 'returns success' do
23+
put("/api/public_projects/#{project.identifier}", headers:, params:)
24+
25+
expect(response).to have_http_status(:success)
26+
end
27+
end
28+
29+
context 'when updating project fails' do
30+
before do
31+
authenticated_in_hydra_as(creator)
32+
33+
response = OperationResponse.new
34+
response[:error] = 'Error updating project'
35+
allow(PublicProject::Update).to receive(:call).and_return(response)
36+
end
37+
38+
it 'returns error' do
39+
put("/api/public_projects/#{project.identifier}", headers:, params:)
40+
41+
expect(response).to have_http_status(:unprocessable_entity)
42+
end
43+
end
44+
end
45+
46+
context 'when no token is given' do
47+
it 'returns unauthorized' do
48+
put("/api/public_projects/#{project.identifier}", headers:, params:)
49+
50+
expect(response).to have_http_status(:unauthorized)
51+
end
52+
end
53+
end

0 commit comments

Comments
 (0)