Skip to content

Commit d1243e1

Browse files
authored
Sortables index
* Add an index method to the user_sortables_controller, add spec, add routes * Add meta method and separate logic for user sortables in a re-usable hash
1 parent 2ef4351 commit d1243e1

File tree

3 files changed

+218
-8
lines changed

3 files changed

+218
-8
lines changed

app/controllers/api/v3/user_sortables_controller.rb

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,48 @@ module V3
66
class UserSortablesController < BaseController
77
include UsesScenario
88

9-
before_action :assert_valid_sortable_type
9+
before_action :assert_valid_sortable_type, only: %i[show update]
1010

1111
rescue_from NoMethodError do |e|
1212
raise e unless e.message.starts_with?("undefined method `permit'")
1313

1414
render json: { errors: ['Invalid JSON payload'] }, status: :bad_request
1515
end
1616

17+
SORTABLES = {
18+
forecast_storage: :forecast_storage_order,
19+
hydrogen_supply: :hydrogen_supply_order,
20+
hydrogen_demand: :hydrogen_demand_order,
21+
space_heating: :households_space_heating_producer_order,
22+
heat_network: :heat_network_order
23+
}.freeze
24+
25+
HEAT_NETWORK_SUBTYPES = %i[lt mt ht].freeze
26+
27+
# GET /api/v3/scenarios/:scenario_id/user_sortables/meta
28+
def meta
29+
render json: {
30+
types: SORTABLES.keys,
31+
heat_network_subtypes: HEAT_NETWORK_SUBTYPES
32+
}
33+
end
34+
35+
# GET /api/v3/scenarios/:scenario_id/user_sortables
36+
# Returns all sortable orders (grouped by type, and by subtype for heat_network)
37+
def index
38+
data = SORTABLES.each_with_object({}) do |(type, method_name), h|
39+
if type == :heat_network
40+
h[type] = HEAT_NETWORK_SUBTYPES.each_with_object({}) do |sub, sub_h|
41+
sub_h[sub] = scenario.public_send(method_name, sub).order
42+
end
43+
else
44+
h[type] = scenario.public_send(method_name).order
45+
end
46+
end
47+
48+
render json: data
49+
end
50+
1751
def show
1852
render json: sortable
1953
end
@@ -56,13 +90,7 @@ def assert_valid_sortable_type
5690
end
5791

5892
def sortable_name
59-
case params[:sortable_type]
60-
when :forecast_storage then :forecast_storage_order
61-
when :heat_network then :heat_network_order
62-
when :hydrogen_supply then :hydrogen_supply_order
63-
when :hydrogen_demand then :hydrogen_demand_order
64-
when :space_heating then :households_space_heating_producer_order
65-
end
93+
SORTABLES[params[:sortable_type]&.to_sym]
6694
end
6795

6896
# Used for the types of heat networks (lt, mt and ht)

config/routes.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@
5252

5353
resource :version, only: %i[create show update], controller: 'scenario_version_tags'
5454

55+
# New unified entrypoint for sortables - my suggestion is to move from the individualised routes below to this approach.
56+
# GET - /api/v3/scenarios/:scenario_id/user_sortables
57+
# GET & PATCH - /api/v3/scenarios/:scenario_id/user_sortables/:user_sortable
58+
resources :user_sortables,
59+
only: %i[index show update],
60+
controller: :user_sortables,
61+
param: :sortable_type do
62+
collection do
63+
get :meta
64+
end
65+
end
66+
5567
resource :heat_network_order, only: %i[show update],
5668
controller: :user_sortables, sortable_type: :heat_network
5769

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe Api::V3::UserSortablesController, type: :controller do
6+
let(:scenario) { create(:scenario) }
7+
let(:user) { create(:user) }
8+
let(:headers) { access_token_header(user, :write) }
9+
10+
before do
11+
request.headers.merge!(headers)
12+
end
13+
14+
describe 'GET #index' do
15+
context 'when no custom orders have been modified' do
16+
before do
17+
get :index, params: { scenario_id: scenario.id }
18+
end
19+
20+
it 'returns HTTP 200' do
21+
expect(response).to have_http_status(:ok)
22+
end
23+
24+
it 'returns all five top-level keys plus heat_network sub-keys, each as an Array' do
25+
json = JSON.parse(response.body)
26+
27+
expect(json.keys).to contain_exactly(
28+
'forecast_storage',
29+
'hydrogen_supply',
30+
'hydrogen_demand',
31+
'space_heating',
32+
'heat_network'
33+
)
34+
35+
expect(json['forecast_storage']).to be_an(Array)
36+
expect(json['hydrogen_supply']).to be_an(Array)
37+
expect(json['hydrogen_demand']).to be_an(Array)
38+
expect(json['space_heating']).to be_an(Array)
39+
40+
heat = json['heat_network']
41+
expect(heat.keys).to contain_exactly('lt', 'mt', 'ht')
42+
heat.each_value { |arr| expect(arr).to be_an(Array) }
43+
end
44+
end
45+
end
46+
47+
describe 'GET #show' do
48+
context 'valid, non-subtyped sortable_type' do
49+
{
50+
forecast_storage: :forecast_storage_order,
51+
hydrogen_supply: :hydrogen_supply_order,
52+
hydrogen_demand: :hydrogen_demand_order,
53+
space_heating: :households_space_heating_producer_order
54+
}.each do |stype, method_name|
55+
it "returns 200 and the #{stype} order" do
56+
get :show,
57+
params: {
58+
scenario_id: scenario.id,
59+
sortable_type: stype
60+
}
61+
62+
expect(response).to have_http_status(:ok)
63+
json = JSON.parse(response.body)
64+
expect(json).to include('order')
65+
expect(json['order']).to eq(
66+
scenario.public_send(method_name).order
67+
)
68+
end
69+
end
70+
end
71+
72+
context 'heat_network with sub-types' do
73+
%i[lt mt ht].each do |sub|
74+
it "returns 200 and heat_network/#{sub} order" do
75+
get :show,
76+
params: {
77+
scenario_id: scenario.id,
78+
sortable_type: :heat_network,
79+
subtype: sub
80+
}
81+
82+
expect(response).to have_http_status(:ok)
83+
json = JSON.parse(response.body)
84+
expect(json['order']).to eq(
85+
scenario.heat_network_order(sub).order
86+
)
87+
end
88+
end
89+
end
90+
91+
context 'unknown sortable_type' do
92+
it 'renders 404 with a Not found error' do
93+
get :show,
94+
params: {
95+
scenario_id: scenario.id,
96+
sortable_type: :not_a_type
97+
}
98+
99+
expect(response).to have_http_status(:not_found)
100+
json = JSON.parse(response.body)
101+
expect(json['errors']).to eq(['Not found'])
102+
end
103+
end
104+
end
105+
106+
describe 'PUT #update' do
107+
let(:new_order) { scenario.forecast_storage_order.order.reverse }
108+
109+
context 'with valid params' do
110+
it 'updates the order and returns 200' do
111+
put :update,
112+
params: {
113+
scenario_id: scenario.id,
114+
sortable_type: :forecast_storage,
115+
order: new_order
116+
}
117+
118+
expect(response).to have_http_status(:ok)
119+
json = JSON.parse(response.body)
120+
expect(json['order']).to eq(new_order)
121+
expect(scenario.reload.forecast_storage_order.order).to eq(new_order)
122+
end
123+
end
124+
125+
context 'when the model is invalid' do
126+
before do
127+
# force a validation failure
128+
order_model = scenario.forecast_storage_order
129+
allow_any_instance_of(order_model.class).to receive(:valid?).and_return(false)
130+
allow_any_instance_of(order_model.class)
131+
.to receive_message_chain(:errors, :full_messages)
132+
.and_return(['failure'])
133+
end
134+
135+
it 'returns 422 with the model errors' do
136+
put :update,
137+
params: {
138+
scenario_id: scenario.id,
139+
sortable_type: :forecast_storage,
140+
order: new_order
141+
}
142+
143+
expect(response).to have_http_status(:unprocessable_entity)
144+
json = JSON.parse(response.body)
145+
expect(json['errors']).to include('failure')
146+
end
147+
end
148+
149+
context 'with an invalid JSON payload' do
150+
before do
151+
allow_any_instance_of(described_class)
152+
.to receive(:sortable_params)
153+
.and_raise(NoMethodError.new("undefined method `permit' for nil:NilClass"))
154+
end
155+
156+
it 'rescues and returns 400 Invalid JSON payload' do
157+
put :update,
158+
params: {
159+
scenario_id: scenario.id,
160+
sortable_type: :forecast_storage,
161+
order: 'not_an_array'
162+
}
163+
164+
expect(response).to have_http_status(:bad_request)
165+
json = JSON.parse(response.body)
166+
expect(json['errors']).to include('Invalid JSON payload')
167+
end
168+
end
169+
end
170+
end

0 commit comments

Comments
 (0)