You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
6.2 KiB
178 lines
6.2 KiB
# lib/questions_crafter/dsl.rb
|
|
module QuestionsCrafter
|
|
module Dsl
|
|
def self.included(base)
|
|
base.class_eval do
|
|
include ActiveModel::Model
|
|
include ActiveModel::Attributes if defined?(ActiveModel::Attributes)
|
|
include ActiveModel::Dirty
|
|
|
|
extend ClassMethods
|
|
|
|
# Add instance method to access class-level questions
|
|
define_method :questions do
|
|
self.class.questions
|
|
end
|
|
|
|
# Add method to get question attributes with values
|
|
define_method :question_attributes do
|
|
result = {}
|
|
self.class.questions.each do |question|
|
|
name = question[:name]
|
|
result[name] = {
|
|
value: self.send(name),
|
|
**question
|
|
}
|
|
end
|
|
result
|
|
end
|
|
|
|
# Add instance method for normalizing checkbox values
|
|
define_method :normalize_checkbox_group_value do |value|
|
|
case value
|
|
when Array
|
|
value
|
|
when String
|
|
if value.start_with?("[") && value.end_with?("]")
|
|
JSON.parse(value) rescue []
|
|
else
|
|
value.present? ? [ value ] : []
|
|
end
|
|
when NilClass, FalseClass
|
|
[]
|
|
else
|
|
value.present? ? [ value ] : []
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
module ClassMethods
|
|
def questions
|
|
@questions ||= []
|
|
end
|
|
|
|
def question_names
|
|
@question_names ||= {}
|
|
end
|
|
|
|
def question(name, as:, title: nil, description: nil, options: {}, required: false)
|
|
type_map = {
|
|
string: QuestionsCrafter::Constants::STRING,
|
|
text: QuestionsCrafter::Constants::TEXT,
|
|
radio: QuestionsCrafter::Constants::RADIO,
|
|
boolean: QuestionsCrafter::Constants::BOOLEAN,
|
|
integer: QuestionsCrafter::Constants::INTEGER,
|
|
select: QuestionsCrafter::Constants::SELECT,
|
|
checkbox_group: QuestionsCrafter::Constants::CHECKBOX_GROUP
|
|
}
|
|
type = type_map[as]
|
|
raise NotImplementedError,
|
|
"Question type '#{as}' is not implemented. Supported types are: #{type_map.keys.inspect}" unless type
|
|
|
|
add_question(name, type: type, title: title, description: description, options: options, required: required)
|
|
end
|
|
|
|
def title(title = nil)
|
|
@title = title if title
|
|
@title || name.demodulize.titleize
|
|
end
|
|
|
|
def description(description = nil)
|
|
@description = description if description
|
|
@description || ""
|
|
end
|
|
|
|
def back_link(link = nil)
|
|
@back_link = link if link
|
|
@back_link || "#"
|
|
end
|
|
|
|
def custom_html_classes(custom_html_classes = nil)
|
|
@custom_html_classes = custom_html_classes if custom_html_classes
|
|
@custom_html_classes || {}
|
|
end
|
|
|
|
private
|
|
|
|
# options: Can be either a hash of options with value as a key and text as the visible text for the option
|
|
# or an array of strings where each string is a symbol and it gets translated using I18n.t
|
|
def add_question(name, type:, options: {}, title: nil, description: nil, required: false)
|
|
raise QuestionAlreadyDefined, "Question #{name} already defined" if question_names[name]
|
|
|
|
question_names[name] = true
|
|
title ||= name.titleize
|
|
question = {
|
|
name: name,
|
|
type: type,
|
|
options: options,
|
|
title: title,
|
|
description: description,
|
|
required: required
|
|
}
|
|
questions << question
|
|
|
|
configure_question(question: question)
|
|
# TODO: Along with the adding of each question we need to set validations and other ActiveModel stuff
|
|
end
|
|
|
|
def configure_question(question:)
|
|
name = question[:name]
|
|
type = question[:type]
|
|
options = question[:options]
|
|
required = question[:required]
|
|
|
|
create_attribute(name, type, options)
|
|
setup_validations(name: name, type: type, options: options, required: required)
|
|
end
|
|
|
|
def setup_validations(name:, type:, options: {}, required:)
|
|
class_name = self.class.name.underscore
|
|
if type == QuestionsCrafter::Constants::CHECKBOX_GROUP
|
|
setup_checkbox_group_validations(name, class_name, options, required)
|
|
else
|
|
validates name, presence: { message: I18n.t("questions.errors.#{class_name}.#{name}.blank", default: I18n.t("errors.messages.blank")) } if required
|
|
validates name, inclusion: { in: options.keys, message: I18n.t("questions.errors.#{class_name}.#{name}.inclusion", default: I18n.t("errors.messages.inclusion")) } if options.any?
|
|
end
|
|
end
|
|
|
|
def setup_checkbox_group_validations(name, class_name, options, required)
|
|
validate do |record|
|
|
values = record.normalize_checkbox_group_value(record.send(name))
|
|
|
|
if required && values.blank?
|
|
record.errors.add(name, I18n.t("questions.errors.#{class_name}.#{name}.blank",
|
|
default: I18n.t("errors.messages.blank")))
|
|
end
|
|
|
|
if values.any?
|
|
invalid_values = values.reject { |v| options.keys.include?(v) }
|
|
if invalid_values.any?
|
|
record.errors.add(name, I18n.t("questions.errors.#{class_name}.#{name}.inclusion",
|
|
default: I18n.t("errors.messages.inclusion")))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# As we've included ActiveModel::Attributes we can use the attribute method to define attributes
|
|
def create_attribute(name, type, options)
|
|
attr_options = {} # TODO: we have options such as: default: true
|
|
attribute name.to_sym, crafter_type_to_attribute_type(type), **attr_options
|
|
end
|
|
|
|
def crafter_type_to_attribute_type(type)
|
|
case type
|
|
when QuestionsCrafter::Constants::STRING, QuestionsCrafter::Constants::TEXT,
|
|
QuestionsCrafter::Constants::SELECT, QuestionsCrafter::Constants::CHECKBOX_GROUP,
|
|
QuestionsCrafter::Constants::RADIO
|
|
:string
|
|
when QuestionsCrafter::Constants::BOOLEAN
|
|
:boolean
|
|
when QuestionsCrafter::Constants::INTEGER
|
|
:integer
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|