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
25 changes: 25 additions & 0 deletions app/controllers/admin/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Admin
class BaseController < ActionController::Base
layout 'admin'
protect_from_forgery with: :exception

http_basic_authenticate_with(
name: Rails.application.config.admin_basic_auth_username,
password: Rails.application.config.admin_basic_auth_password
)

helper_method :pretty_json

private

def params_page_size
(params[:page_size] || 25).to_i.clamp(0, 100)
end

def pretty_json(value)
JSON.pretty_generate(value.presence || {})
end
end
end
87 changes: 87 additions & 0 deletions app/controllers/admin/contexts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

module Admin
class ContextsController < BaseController
rescue_from ::Contexts::CreateAttributes::ValidationError,
::Contexts::UpdateAttributes::ValidationError,
Batch::RuntimeConfig::ValidationError,
ActiveRecord::RecordInvalid do |e|
flash.now[:error] = e.message
@context =
if params[:id]
Context.find(params[:id])
else
Context.new(context_form_params.except('metadata'))
end
render action_name == 'create' ? :new : :edit, status: :unprocessable_entity
end
rescue_from ActiveRecord::DeleteRestrictionError do |e|
redirect_to admin_context_path(params[:id]), alert: e.message
end

def index
@contexts = Context.order(id: :desc).limit(params_page_size)
end

def show
@context = Context.find(params[:id])
@matching_definitions = matching_definitions(@context)
end

def new
@context = Context.new(
metadata: {
'batch' => {}
}
)
end

def create
@context = ::Contexts::CreateAttributes.new(params: context_form_params).call
redirect_to admin_context_path(@context), notice: 'Context created'
end

def edit
@context = Context.find(params[:id])
end

def update
@context = Context.find(params[:id])
::Contexts::UpdateAttributes.new(context: @context, params: context_form_params).call
redirect_to admin_context_path(@context), notice: 'Context updated'
end

def destroy
context = Context.find(params[:id])
context.destroy!
redirect_to admin_contexts_path, notice: 'Context deleted'
end

private

def context_form_params
permitted = params.require(:context).permit(
*::Contexts::UpdateAttributes::CONTEXT_ATTRIBUTE_KEYS,
:metadata_json
)

raw = permitted.to_h
metadata_json = raw.delete('metadata_json')
raw['metadata'] = parse_json_field(metadata_json, field_name: 'metadata') if metadata_json.present?
raw
end

def parse_json_field(raw_json, field_name:)
JSON.parse(raw_json)
rescue JSON::ParserError => e
raise ::Contexts::UpdateAttributes::ValidationError, "#{field_name} must be valid JSON: #{e.message}"
end

def matching_definitions(context)
LabelExtractorDefinition.where(
module_name: context.module_name,
extractor_name: context.extractor_name
).order(updated_at: :desc)
end
end
end
105 changes: 105 additions & 0 deletions app/controllers/admin/label_extractor_definitions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module Admin
class LabelExtractorDefinitionsController < BaseController
class ValidationError < StandardError; end

rescue_from ActiveRecord::RecordInvalid do
flash.now[:error] = @definition.errors.full_messages.to_sentence
prepare_form_state
render action_name == 'update' ? :edit : :new, status: :unprocessable_entity
end
rescue_from ValidationError do |e|
flash.now[:error] = e.message
prepare_form_state
render action_name == 'create' ? :new : :edit, status: :unprocessable_entity
end

def index
@definitions = LabelExtractorDefinition.order(updated_at: :desc).limit(params_page_size)
end

def new
@definition = LabelExtractorDefinition.new(enabled: true)
end

def create
@definition = LabelExtractorDefinition.new(definition_attributes)
@definition.save!
redirect_to admin_label_extractor_definitions_path, notice: 'Label extractor definition created'
end

def edit
@definition = LabelExtractorDefinition.find(params[:id])
@return_to = resolved_return_to
end

def update
@definition = LabelExtractorDefinition.find(params[:id])
@definition.update!(definition_attributes)
redirect_to resolved_return_to, notice: 'Label extractor definition updated'
end

def destroy
definition = LabelExtractorDefinition.find(params[:id])
definition.destroy!
redirect_to admin_label_extractor_definitions_path, notice: 'Label extractor definition deleted'
end

private

def definition_form_params
params.require(:label_extractor_definition).permit(
:module_name,
:extractor_name,
:enabled,
:config_json
)
end

def definition_attributes
{
module_name: definition_form_params[:module_name],
extractor_name: definition_form_params[:extractor_name],
enabled: truthy?(definition_form_params[:enabled]),
config: parse_json_field(definition_form_params[:config_json], field_name: 'config')
}
end

def parse_json_field(raw_json, field_name:)
JSON.parse(raw_json.presence || '{}')
rescue JSON::ParserError => e
raise ValidationError, "#{field_name} must be valid JSON: #{e.message}"
end

def truthy?(value)
ActiveModel::Type::Boolean.new.cast(value)
end

def prepare_form_state
@definition ||= params[:id] ? LabelExtractorDefinition.find(params[:id]) : LabelExtractorDefinition.new(enabled: true)
@return_to = resolved_return_to
return unless params[:label_extractor_definition]

@definition.assign_attributes(
module_name: definition_form_params[:module_name],
extractor_name: definition_form_params[:extractor_name],
enabled: truthy?(definition_form_params[:enabled])
)
@config_json_value = definition_form_params[:config_json]
end

def resolved_return_to
requested_path = params[:return_to].presence
return admin_label_extractor_definitions_path if requested_path.blank?

uri = URI.parse(requested_path)
return admin_label_extractor_definitions_path if uri.host.present? || uri.scheme.present?
return admin_label_extractor_definitions_path unless uri.path.start_with?('/admin')

uri.to_s
rescue URI::InvalidURIError
admin_label_extractor_definitions_path
end
end
end
85 changes: 5 additions & 80 deletions app/controllers/contexts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
# frozen_string_literal: true

class ContextsController < ApplicationController
class ValidationError < StandardError; end

CONTEXT_ATTRIBUTE_KEYS = %w[
workflow_id
project_id
active_subject_set_id
pool_subject_set_id
module_name
extractor_name
].freeze
INTEGER_ATTRIBUTE_KEYS = %w[
workflow_id
project_id
active_subject_set_id
pool_subject_set_id
].freeze
wrap_parameters false

# as we're running in API mode we need to include basic auth
Expand All @@ -27,13 +11,9 @@ class ValidationError < StandardError; end
password: Rails.application.config.api_basic_auth_password
)

rescue_from ValidationError do |e|
json_error_render(:unprocessable_entity, e)
end
rescue_from Batch::RuntimeConfig::ValidationError do |e|
json_error_render(:unprocessable_entity, e)
end
rescue_from ActiveRecord::RecordInvalid do |e|
rescue_from Contexts::UpdateAttributes::ValidationError,
Batch::RuntimeConfig::ValidationError,
ActiveRecord::RecordInvalid do |e|
json_error_render(:unprocessable_entity, e)
end

Expand All @@ -54,18 +34,8 @@ def show
end

def update
validate_context_params!

context = Context.find(params[:id])
attributes = context_params.slice(*CONTEXT_ATTRIBUTE_KEYS)

if context_params.key?('metadata')
metadata = context_metadata(context).deep_merge(context_params.fetch('metadata').to_h)
validate_batch_metadata!(metadata['batch']) if context_params.fetch('metadata').to_h.key?('batch')
attributes[:metadata] = metadata
end

context.update!(attributes)
Contexts::UpdateAttributes.new(context: context, params: context_params).call

render(
status: :ok,
Expand All @@ -77,54 +47,9 @@ def update

def context_params
@context_params ||= params.permit(
*CONTEXT_ATTRIBUTE_KEYS,
*Contexts::UpdateAttributes::CONTEXT_ATTRIBUTE_KEYS,
:metadata,
metadata: {}
)
end

def validate_context_params!
raise ValidationError, 'at least one supported context field is required' if context_params.empty?

if context_params.key?('metadata') && !context_params['metadata'].respond_to?(:to_h)
raise ValidationError, 'metadata must be an object'
end

INTEGER_ATTRIBUTE_KEYS.each { |key| validate_integer_param!(key) if context_params.key?(key) }
validate_extractor_pair!
end

def context_metadata(context)
context.metadata.is_a?(Hash) ? context.metadata.deep_dup : {}
end

def validate_batch_metadata!(batch_config)
return if batch_config.nil?

unless batch_config.respond_to?(:to_h)
raise ValidationError, 'metadata.batch must be an object'
end

Batch::RuntimeConfig.validate!(batch_config)
end

def validate_extractor_pair!
return unless context_params.key?('module_name') || context_params.key?('extractor_name')

context = Context.find(params[:id])
module_name = context_params.key?('module_name') ? context_params['module_name'] : context.module_name
extractor_name = context_params.key?('extractor_name') ? context_params['extractor_name'] : context.extractor_name

return if LabelExtractors::Registry.extractor_registered?(module_name, extractor_name)

raise ValidationError, "unknown module/extractor pair: #{module_name}/#{extractor_name}"
end

def validate_integer_param!(key)
value = context_params[key]
return if value.is_a?(Integer)
return if value.is_a?(String) && value.match?(/\A\d+\z/)

raise ValidationError, "#{key} must be an integer"
end
end
9 changes: 5 additions & 4 deletions app/modules/label_extractors/configurable_extractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ def self.normalize_config(config)
def self.validate_config!(config)
raise ConfigurationError, 'config must be an object' unless config.is_a?(Hash)

validate_string!(config, 'data_release_suffix')
validate_hash!(config, 'task_key_label_prefixes')
validate_hash!(config, 'task_key_data_labels')
validate_task_mappings!(config)
# commented out for now to allow for more flexible config
# validate_string!(config, 'data_release_suffix')
# validate_hash!(config, 'task_key_label_prefixes')
# validate_hash!(config, 'task_key_data_labels')
# validate_task_mappings!(config)
end

def self.validate_string!(config, key)
Expand Down
Loading
Loading