Skip to content

Commit

Permalink
API Search and Add for Providers
Browse files Browse the repository at this point in the history
Add Provider page with auto complete

WIP

WIP

Changes since support user authentication

Move capybara settings

Changes to API service to get individual providers based on code

Add Publish URL to env.test

Conflicting linters

Add translated error message for provider code taken

Resolving Prettier lint issues

Remove message from provider validation

Resolve prettier issue

Resolve prettier issue

Update yarn lock

Lint since rebase

Yarn update

Use next year in api tests
  • Loading branch information
Jamie committed Jan 2, 2024
1 parent 26c3245 commit 95da5f3
Show file tree
Hide file tree
Showing 28 changed files with 674 additions and 8 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ SIGN_IN_METHOD=persona
PLACEMENTS_HOST=placements.localhost
CLAIMS_HOST=claims.localhost
GIAS_CSV_BASE_URL=https://ea-edubase-api-prod.azurewebsites.net/edubase/downloads/public
PUBLISH_BASE_URL=https://www.publish-teacher-training-courses.service.gov.uk
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
PLACEMENTS_HOST=placements.localhost
CLAIMS_HOST=claims.localhost
PUBLISH_BASE_URL=https://www.publish-teacher-training-courses.service.gov.uk
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ gem "draper"
# Download files safely
gem "down", "~> 5.0"

# HTTP Request
gem "httparty"

group :development do
gem "annotate", require: false
gem "prettier_print", require: false
Expand Down Expand Up @@ -91,6 +94,7 @@ group :test do
gem "shoulda-matchers"
# launch browser when inspecting capybara specs
gem "launchy"
gem "webmock"
end

group :test, :development do
Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ GEM
coderay (1.1.3)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crack (0.4.5)
rexml
crass (1.0.6)
cssbundling-rails (1.3.3)
railties (>= 6.0.0)
Expand Down Expand Up @@ -180,9 +182,13 @@ GEM
temple (>= 0.8.2)
thor
tilt
hashdiff (1.0.1)
hashie (5.0.0)
html-attributes-utils (1.0.2)
activesupport (>= 6.1.4.4)
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
io-console (0.6.0)
Expand Down Expand Up @@ -228,6 +234,7 @@ GEM
mini_mime (1.1.5)
minitest (5.20.0)
msgpack (1.7.2)
multi_xml (0.6.0)
mutex_m (0.2.0)
net-imap (0.4.8)
date
Expand Down Expand Up @@ -495,6 +502,10 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.19.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1)
websocket (1.2.10)
websocket-driver (0.7.6)
Expand Down Expand Up @@ -526,6 +537,7 @@ DEPENDENCIES
flipflop
govuk-components (~> 5.0.0)
govuk_design_system_formbuilder (~> 5.0.0)
httparty
jsbundling-rails
launchy
mail-notify
Expand Down Expand Up @@ -557,6 +569,7 @@ DEPENDENCIES
tzinfo-data
view_component
web-console
webmock

RUBY VERSION
ruby 3.2.2p53
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/application.sass.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ $govuk-new-link-styles: true;
$govuk-assets-path: "";

@import "govuk-frontend/dist/govuk/all";

@import "components/all";
@import "accessible-autocomplete/dist/accessible-autocomplete.min";
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class Placements::Support::ProviderSuggestionsController < Placements::Support::ApplicationController
def index
accredited_providers = AccreditedProviderApi.call
filtered_providers = filter_providers(accredited_providers, query_params)
providers =
filtered_providers.map { |provider| formatted_provider(provider) }
render json: providers
end

private

def query_params
params.require(:query)
end

def filter_providers(providers, query)
return providers if query.blank?

downcase_query = query.downcase
providers.select do |provider|
[
provider.dig("attributes", "name"),
provider.dig("attributes", "postcode"),
provider.dig("attributes", "urn"),
provider.dig("attributes", "ukprn"),
].any? { |attribute| attribute&.downcase&.include?(downcase_query) }
end
end

def formatted_provider(provider)
{
id: provider.fetch("id"),
name: provider.dig("attributes", "name"),
code: provider.dig("attributes", "code"),
}
end
end
28 changes: 28 additions & 0 deletions app/controllers/placements/support/providers_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Placements::Support::ProvidersController < Placements::Support::ApplicationController
def new
@provider = Provider.new
end

def create
@provider = Provider.new(provider_params)
if @provider.save
redirect_to placements_support_organisations_path
else
render :new
end
end

def check
@provider = Provider.new(provider_code: params[:accredited_provider_id])
@provider_details = AccreditedProviderApi.call(@provider.provider_code)
return if @provider.valid? && @provider_details.present?

render :new
end

private

def provider_params
params.require(:provider).permit(:provider_code)
end
end
28 changes: 28 additions & 0 deletions app/javascript/accredited_provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import initAutocomplete from "./autocomplete";

const providerTemplate = (result) => result && result.name;
const providerSuggestionTemplate = (result) =>
result && `${result.name} (${result.code})`;
const onConfirm = (input) => (option) =>
(input.value = option ? option.code : "");

function init() {
const options = {
path: `/support/provider_suggestions`,
template: {
inputValue: providerTemplate,
suggestion: providerSuggestionTemplate,
},
minLength: 2,
inputName: "accredited_provider_id",
onConfirm,
};

initAutocomplete(
"accredited-provider-autocomplete",
"accredited-provider-search-form-query-field",
options,
);
}

export default init;
4 changes: 4 additions & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { initAll } from "govuk-frontend";
import autocompleteSetup from "./autocomplete";
import accreditedProviderAutocompleteSetup from "./accredited_provider";

initAll();
autocompleteSetup();
accreditedProviderAutocompleteSetup();
41 changes: 41 additions & 0 deletions app/javascript/autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import accessibleAutocomplete from "accessible-autocomplete";
import debounce from "lodash.debounce";
import { request } from "./utils/request_helper";

const initAutocomplete = (elementId, inputId, options = {}) => {
try {
const element = document.getElementById(elementId);
const input = document.getElementById(inputId);

if (element && input) {
const { path, template, minLength, onConfirm, inputName } = options;
const { inputValue, suggestion } = template;

accessibleAutocomplete({
element,
id: input.id,
showNoOptionsFound: true,
name: input.name,
defaultValue: input.value,
minLength,
source: debounce(request(path), 900),
templates: {
inputValue,
suggestion,
},
onConfirm: onConfirm(input),
confirmOnBlur: false,
autoselect: true,
});

// Hijack the original input to submit the selected value.
input.id = `old-${input.id}`;
input.name = inputName;
input.type = "hidden";
}
} catch (err) {
console.error(`Failed to initialise autocomplete for ${elementId}:`, err);
}
};

export default initAutocomplete;
32 changes: 32 additions & 0 deletions app/javascript/utils/request_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const getPath = (endpoint, query) => {
return `${endpoint}?query=${query}`;
};

const request = (endpoint) => {
let xhr = null; // Hoist this call so that we can abort previous requests.

return (query, callback) => {
if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
xhr.abort();
}
const path = getPath(endpoint, query);

xhr = new XMLHttpRequest();
xhr.addEventListener("load", (evt) => {
let results = [];
try {
results = JSON.parse(xhr.responseText);
} catch (err) {
console.error(
`Failed to parse results from endpoint ${path}, error is:`,
err,
);
}
callback(results);
});
xhr.open("GET", path);
xhr.send();
};
};

export { getPath, request };
47 changes: 47 additions & 0 deletions app/services/accredited_provider_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class AccreditedProviderApi
include ServicePattern

def initialize(code = nil)
@code = code
end

attr_reader :code

def call
code.present? ? provider_list(code) : provider_details
end

private

def provider_list(code)
Rails
.cache
.fetch("accredited_provider_details_#{code}", expires_in: 24.hours) do
response = HTTParty.get(provider_details_url(code))
response = JSON.parse(response.to_s)
response["data"]
end
end

def provider_details
Rails
.cache
.fetch("all_accredited_providers", expires_in: 24.hours) do
response = HTTParty.get(all_providers_url)
response = JSON.parse(response.to_s)
response["data"]
end
end

def all_providers_url
"#{ENV["PUBLISH_BASE_URL"]}/api/public/v1/recruitment_cycles/#{next_year}/providers?filter[is_accredited_body]=true"
end

def provider_details_url(code)
"#{ENV["PUBLISH_BASE_URL"]}/api/public/v1/recruitment_cycles/#{next_year}/providers/#{code}"
end

def next_year
Time.current.next_year.year
end
end
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
</div>

<div class="govuk-width-container">
<%= yield :before_content %>
<main class="govuk-main-wrapper" id="main-content" role="main">
<%= yield %>
</main>
Expand Down
Loading

0 comments on commit 95da5f3

Please sign in to comment.