Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,6 @@ gem "cssbundling-rails", "~> 1.4"
# Aborts requests that are taking too long.
# Set the timeout by setting the RACK_TIMEOUT_SERVICE_TIMEOUT env var
# gem "rack-timeout"

# Http client for making API requests
gem "faraday", "~> 2.13.1"
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ GEM
railties (>= 5.0.0)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
faraday (2.13.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
ffi (1.16.3)
geo_coord (0.2.0)
geocoder (1.8.2)
Expand Down Expand Up @@ -163,6 +169,7 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand All @@ -178,6 +185,8 @@ GEM
mini_mime (1.1.5)
minitest (5.25.1)
msgpack (1.7.2)
net-http (0.6.0)
uri
net-imap (0.4.10)
date
net-protocol
Expand Down Expand Up @@ -349,6 +358,7 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
uri (1.0.3)
view_component (3.11.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -387,6 +397,7 @@ DEPENDENCIES
dotenv-rails
factory_bot_rails (~> 6.4.3)
faker (~> 3.4.2)
faraday (~> 2.13.1)
geo_coord
geocoder (~> 1.8)
haversine!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@

<div class="navbar-end">
<% if helpers.user_signed_in? %>
<% if helpers.current_user.admin? %>
<div class="navbar-item">
<%= link_to 'Tools', admin_tools_path, class: 'button is-light' %>
</div>
<% end %>

<div class="navbar-item">
<%= link_to admin_user_path(current_user), class: 'button is-light' do %>
<span>Logged in as <strong><%= helpers.current_user.email %></strong></span>
Expand Down
9 changes: 4 additions & 5 deletions app/controllers/admin/facilities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,10 @@ def load_facilities
facilities = facilities.discarded
end

if params[:service_id] == "none"
if params[:service] == "none"
facilities = facilities.without_services
elsif params[:service_id].present?
facilities = facilities.joins(:services)
.where(services: { id: params[:service_id] })
elsif params[:service].present?
facilities = facilities.with_service(params[:service])
end

if params[:welcome_customer] == "none"
Expand All @@ -115,7 +114,7 @@ def load_facility
end

def load_services_dropdown
@services_dropdown = [["No Services", :none]] + Service.pluck(:name, :id)
@services_dropdown = [["No Services", :none]] + Service.pluck(:name, :key)
end

def load_welcomes_dropdown
Expand Down
45 changes: 45 additions & 0 deletions app/controllers/admin/tools_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

class Admin::ToolsController < Admin::BaseController
before_action :enforce_admin_user

def index

end

def import_facilities
api_key = params[:api]

# Validate that both parameters are present and supported
unless External::ApiHelper.supported_api?(api_key)
redirect_to admin_tools_path, alert: "Invalid API selected. Please choose from the supported APIs."
return
end

result = External::VancouverCity::Syncer.call(
api_key: api_key,
api_client: External::VancouverCity.default_client
)

if result.success?
total_count = result.data[:total_count] || 0
redirect_to admin_facilities_path(service: "water_fountain"), notice: "#{total_count} Facilities imported successfully from #{External::ApiHelper.api_name(api_key)}."
else
error_messages = result.errors.join(', ')
redirect_to admin_tools_path, alert: "Failed to import facilities: #{error_messages}"
end
end

# Helper method for the view
helper_method :api_options_for_select

private

def api_options_for_select
External::ApiHelper.api_options
end

def enforce_admin_user
redirect_to root_path, alert: "Access denied! You must be an admin to access tools" unless current_user&.admin?
end
end
6 changes: 6 additions & 0 deletions app/models/facility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class Facility < ApplicationRecord
scope :with_service, ->(service_key_or_name) { joins(:services).where(services: Service.exact_search(service_key_or_name)) }
scope :without_services, -> { where.not(facility_services: FacilityService.all) }
scope :without_welcomes, -> { where.not(facility_welcomes: FacilityWelcome.all) }
scope :external, -> { where.not(external_id: nil) }
scope :not_external, -> { where(external_id: nil) }

def managed_by?(user)
f_user_id = if user.respond_to? :id
Expand Down Expand Up @@ -68,6 +70,10 @@ def self.statuses
%i[live pending_reviews discarded]
end

def external?
external_id.present?
end

def status
if discarded?
:discarded
Expand Down
51 changes: 51 additions & 0 deletions app/services/external/api_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

# Configuration class for supported Vancouver City APIs and facility services
class External::ApiHelper
# Available Vancouver City Facilities APIs
# Each API represents a different type of facility data available from Vancouver's Open Data portal
SUPPORTED_APIS = {
'drinking-fountains' => 'Drinking Fountains'
}.freeze

# Mapping of dataset IDs to service keys
# This mapping is used to associate API keys with specific service types in the system
DATASET_ID_TO_SERVICE_KEY = {
'drinking-fountains' => 'water_fountain'
}.freeze

class << self
# Get all supported API options for select fields
# @return [Array<Array>] Array of [display_name, api_key] pairs
def api_options
SUPPORTED_APIS.map { |key, name| [name, key] }
end

# Get all supported API keys
# @return [Array<String>] Array of API keys
def supported_api_keys
SUPPORTED_APIS.keys
end

# Check if an API is supported
# @param api_key [String] The API key to check
# @return [Boolean] True if the API is supported
def supported_api?(api_key)
SUPPORTED_APIS.key?(api_key.to_s)
end

# Get the service key for a given API key
# @param api_key [String] The API key to find the service key for
# @return [String, nil] The service key or nil if not found
def service_key_for(api_key)
DATASET_ID_TO_SERVICE_KEY.dig(api_key.to_s)
end

# Get the display name for an API
# @param api_key [String] The API key
# @return [String, nil] The display name or nil if not found
def api_name(api_key)
SUPPORTED_APIS[api_key.to_s]
end
end
end
9 changes: 9 additions & 0 deletions app/services/external/vancouver_city.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

# Namespace for Vancouver City API integration services
module External::VancouverCity
# Convenience method to get default API client
def self.default_client
VancouverApiClient.default_client
end
end
146 changes: 146 additions & 0 deletions app/services/external/vancouver_city/adapters/faraday_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

require 'faraday'

module External::VancouverCity
module Adapters
# Faraday HTTP adapter for the Vancouver API client
# Uses the builder pattern for flexible configuration
class FaradayAdapter
attr_reader :connection

def initialize(connection)
@connection = connection
end

def self.create(vancouver_api_config)
builder(vancouver_api_config.base_url)
.timeout(vancouver_api_config.timeout)
.open_timeout(vancouver_api_config.open_timeout)
.build
end

# Builder class for creating configured Faraday connections
class Builder
DEFAULT_TIMEOUT = 30
DEFAULT_OPEN_TIMEOUT = 10
DEFAULT_USER_AGENT = 'Linkvan API Client'

def initialize(base_url)
@base_url = base_url
@timeout = DEFAULT_TIMEOUT
@open_timeout = DEFAULT_OPEN_TIMEOUT
@user_agent = DEFAULT_USER_AGENT
@headers = {}
@adapter = Faraday.default_adapter
end
# Set request timeout
# @param timeout [Integer] Request timeout in seconds
# @return [Builder] self for method chaining
def timeout(timeout)
@timeout = timeout
self
end

# Set connection timeout
# @param open_timeout [Integer] Connection timeout in seconds
# @return [Builder] self for method chaining
def open_timeout(open_timeout)
@open_timeout = open_timeout
self
end

# Set user agent string
# @param user_agent [String] User agent for requests
# @return [Builder] self for method chaining
def user_agent(user_agent)
@user_agent = user_agent
self
end

# Add custom header
# @param name [String] Header name
# @param value [String] Header value
# @return [Builder] self for method chaining
def header(name, value)
@headers[name] = value
self
end

# Set Faraday adapter
# @param adapter [Symbol, Object] Faraday adapter
# @return [Builder] self for method chaining
def adapter(adapter)
@adapter = adapter
self
end

# Build the configured Faraday connection
# @return [FaradayAdapter] Configured adapter instance
def build
connection = Faraday.new(url: @base_url) do |config|
config.adapter @adapter

# Set timeouts
config.options.timeout = @timeout
config.options.open_timeout = @open_timeout

# Set default headers
config.headers['User-Agent'] = @user_agent
config.headers['Accept'] = 'application/json'

# Add custom headers
@headers.each do |name, value|
config.headers[name] = value
end
end

FaradayAdapter.new(connection)
end
end

# Create a new builder for the given base URL
# @param base_url [String] The base URL for the API
# @return [Builder] A new builder instance
def self.builder(base_url)
Builder.new(base_url)
end

# Delegate HTTP methods to the Faraday connection
def get(path, params = {})
@connection.get(path, params)
end

def post(path, body = nil, params = {})
@connection.post(path, body, params)
end

def put(path, body = nil, params = {})
@connection.put(path, body, params)
end

def delete(path, params = {})
@connection.delete(path, params)
end

def patch(path, body = nil, params = {})
@connection.patch(path, body, params)
end

# Access connection options for testing
def options
@connection.options
end

# Access connection headers for testing
def headers
@connection.headers
end

# Access connection URL prefix for testing
def url_prefix
@connection.url_prefix
end
end
end
end
Loading