alembic 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/alembic/one_page.js +39 -0
- data/app/controllers/alembic/application_controller.rb +2 -0
- data/app/controllers/alembic/flows_controller.rb +31 -0
- data/app/controllers/alembic/manage/base_controller.rb +9 -0
- data/app/controllers/alembic/manage/flows_controller.rb +37 -3
- data/app/controllers/alembic/manage/page_blocks_controller.rb +1 -1
- data/app/controllers/alembic/manage/pages_controller.rb +7 -0
- data/app/controllers/concerns/alembic/summarizes.rb +35 -2
- data/app/helpers/alembic/application_helper.rb +17 -2
- data/app/helpers/alembic/content_blocks_helper.rb +31 -0
- data/app/helpers/alembic/pages_helper.rb +7 -6
- data/app/helpers/alembic/result_blocks_helper.rb +57 -0
- data/app/models/alembic/flow/definition_summary.rb +2 -0
- data/app/models/alembic/flow/output_form.rb +74 -0
- data/app/models/alembic/flow/preview_values.rb +47 -0
- data/app/models/alembic/flow/summaries.rb +39 -0
- data/app/models/alembic/outputs/band.rb +1 -1
- data/app/models/alembic/outputs/lowest.rb +5 -3
- data/app/models/alembic/outputs/outcome.rb +33 -0
- data/app/models/alembic/page/content_blocks.rb +20 -0
- data/app/models/alembic/page/options.rb +14 -7
- data/app/models/alembic/page/preview.rb +15 -0
- data/app/models/alembic/result_blocks.rb +33 -0
- data/app/models/alembic/summary/report.rb +1 -1
- data/app/models/alembic/summary/result.rb +1 -1
- data/app/views/alembic/blocks/_answers.html.erb +11 -0
- data/app/views/alembic/blocks/_band.html.erb +6 -0
- data/app/views/alembic/blocks/_categories.html.erb +11 -0
- data/app/views/alembic/blocks/_code.html.erb +1 -0
- data/app/views/alembic/blocks/_facts.html.erb +5 -0
- data/app/views/alembic/blocks/_hero.html.erb +9 -0
- data/app/views/alembic/blocks/_lead.html.erb +15 -0
- data/app/views/alembic/blocks/_outcome_entry.html.erb +44 -0
- data/app/views/alembic/blocks/_outcomes.html.erb +10 -0
- data/app/views/alembic/blocks/_score.html.erb +7 -0
- data/app/views/alembic/blocks/_table.html.erb +20 -0
- data/app/views/alembic/blocks/_weakest.html.erb +12 -0
- data/app/views/alembic/flows/_question.html.erb +24 -0
- data/app/views/alembic/flows/complete.html.erb +9 -3
- data/app/views/alembic/flows/one_page.html.erb +35 -0
- data/app/views/alembic/flows/show.html.erb +16 -13
- data/app/views/alembic/flows/step.html.erb +47 -0
- data/app/views/alembic/flows/summary_page.html.erb +5 -0
- data/app/views/alembic/manage/flows/_outputs.html.erb +61 -0
- data/app/views/alembic/manage/flows/edit.html.erb +30 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -1
- data/app/views/alembic/manage/pages/show.html.erb +10 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20260930120000_add_summary_page_to_alembic_flow_definition_summaries.rb +6 -0
- data/db/migrate/20260930230000_add_shows_answer_values_to_alembic_flow_definition_summaries.rb +5 -0
- data/db/migrate/20260930233000_add_asks_on_one_page_to_alembic_flow_definition_summaries.rb +5 -0
- data/db/migrate/20261001120000_add_intro_page_to_alembic_flow_definition_summaries.rb +6 -0
- data/lib/alembic/engine.rb +10 -0
- data/lib/alembic/version.rb +1 -1
- data/lib/alembic.rb +5 -1
- metadata +33 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 199da5d98e867968625a2b5c7052a2b9be9be053dae21f2d9335d0985c821934
|
|
4
|
+
data.tar.gz: d267ef04676182a8973f0d69a50ddb774e26ca8ebe2f252325bb04be8dab62a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7e095677fe4cf0074adb09b2dc098da95b580c1ef5effc567425e712b6f7ccc9fd812fb17067c1b5971d136764e7e9de31580f241d921da76af18f9dd5e62c3
|
|
7
|
+
data.tar.gz: b68e766a8b94d801637d278111ceb983e97a7365b4ee8d66219c1cfce334c453f8b531b6db66d499ac76a3540cd181ee27435ac05b85514da5726c78b3295420
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const start = (form) => {
|
|
3
|
+
if (form.dataset.onePageStarted) return
|
|
4
|
+
form.dataset.onePageStarted = "true"
|
|
5
|
+
|
|
6
|
+
const questions = Array.from(form.querySelectorAll("[data-one-page-question]"))
|
|
7
|
+
const ready = form.querySelector("[data-one-page-ready]")
|
|
8
|
+
const show = (index) => {
|
|
9
|
+
questions.forEach((question, position) => { question.hidden = position !== index })
|
|
10
|
+
ready.hidden = index < questions.length
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
questions.forEach((question, position) => {
|
|
14
|
+
question.querySelectorAll("input[type=radio]").forEach((answer) => {
|
|
15
|
+
answer.addEventListener("change", () => show(position + 1))
|
|
16
|
+
})
|
|
17
|
+
const previous = question.querySelector("[data-one-page-previous]")
|
|
18
|
+
if (previous) {
|
|
19
|
+
previous.hidden = false
|
|
20
|
+
previous.addEventListener("click", () => show(position - 1))
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const back = ready.querySelector("[data-one-page-previous]")
|
|
25
|
+
back.hidden = false
|
|
26
|
+
back.addEventListener("click", () => show(questions.length - 1))
|
|
27
|
+
|
|
28
|
+
show(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const begin = () => document.querySelectorAll("form[data-one-page]").forEach(start)
|
|
32
|
+
|
|
33
|
+
document.addEventListener("turbo:load", begin)
|
|
34
|
+
if (document.readyState === "loading") {
|
|
35
|
+
document.addEventListener("DOMContentLoaded", begin)
|
|
36
|
+
} else {
|
|
37
|
+
begin()
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
@@ -2,9 +2,40 @@ module Alembic
|
|
|
2
2
|
class FlowsController < EasyFlow::FlowsController
|
|
3
3
|
hosted_by FLOW_HOST
|
|
4
4
|
include Summarizes
|
|
5
|
+
helper_method :step_category, :shows_answer_values?
|
|
6
|
+
|
|
7
|
+
def step
|
|
8
|
+
return super unless asked_on_one_page?
|
|
9
|
+
|
|
10
|
+
@guide = runner_for(running_definition)
|
|
11
|
+
@questions = questions_in_order
|
|
12
|
+
render :one_page
|
|
13
|
+
end
|
|
5
14
|
|
|
6
15
|
private
|
|
7
16
|
|
|
17
|
+
def asked_on_one_page?
|
|
18
|
+
run.nil? && params[:answers].blank? && Flow::Summaries.new(flow).asks_on_one_page?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def questions_in_order
|
|
22
|
+
state = {}
|
|
23
|
+
questions = []
|
|
24
|
+
while (question = @guide.next_step(state))
|
|
25
|
+
questions << question
|
|
26
|
+
state[question.id.to_s] = "asked"
|
|
27
|
+
end
|
|
28
|
+
questions
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shows_answer_values?
|
|
32
|
+
Flow::Summaries.new(run&.flow || flow).shows_answer_values?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def step_category(question)
|
|
36
|
+
EasyFlow::Steps::Question.category_of(@guide.step(question.id)&.config)
|
|
37
|
+
end
|
|
38
|
+
|
|
8
39
|
def run_location(run)
|
|
9
40
|
alembic.run_path(run)
|
|
10
41
|
end
|
|
@@ -12,6 +12,15 @@ module Alembic
|
|
|
12
12
|
helper KeystoneUi::React::MountHelper
|
|
13
13
|
helper KsBlocks::ContentHelper
|
|
14
14
|
helper Alembic::PagesHelper
|
|
15
|
+
helper Alembic::ResultBlocksHelper
|
|
16
|
+
helper Alembic::ContentBlocksHelper
|
|
17
|
+
helper_method :preview_values
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def preview_values(page)
|
|
22
|
+
@preview_values ||= Page::Preview.values_for(page, session.dig(:alembic_preview, page.id.to_s))
|
|
23
|
+
end
|
|
15
24
|
end
|
|
16
25
|
end
|
|
17
26
|
end
|
|
@@ -2,16 +2,50 @@ module Alembic
|
|
|
2
2
|
module Manage
|
|
3
3
|
class FlowsController < EasyFlow::Manage::FlowsController
|
|
4
4
|
hosted_by FLOW_HOST
|
|
5
|
+
helper Alembic::ApplicationHelper
|
|
5
6
|
def edit
|
|
6
7
|
super
|
|
7
|
-
|
|
8
|
+
summaries = Flow::Summaries.new(@flow)
|
|
9
|
+
@summary = summaries.text
|
|
10
|
+
@summary_page = summaries.summary_page
|
|
11
|
+
@intro_page = summaries.intro_page
|
|
12
|
+
@outputs = Array(summaries.document.to_h["outputs"])
|
|
13
|
+
@uncategorised = scored_by_category?(@outputs) ? questions_of(@flow).reject { |node| EasyFlow::Steps::Question.category_of(node).present? } : []
|
|
14
|
+
@categories = questions_of(@flow).filter_map { |node| EasyFlow::Steps::Question.category_of(node) }.uniq
|
|
15
|
+
@shows_answer_values = summaries.shows_answer_values?
|
|
16
|
+
@asks_on_one_page = summaries.asks_on_one_page?
|
|
17
|
+
@branches = summaries.branches?
|
|
18
|
+
@published_pages = Page.where(id: Page::Version.live.select(:page_id)).order(:name)
|
|
8
19
|
end
|
|
9
20
|
|
|
10
21
|
def update
|
|
11
|
-
|
|
12
|
-
Flow::Summaries.new(flow_host.flows.find(params[:id]))
|
|
22
|
+
details = params.require(:flow)
|
|
23
|
+
summaries = Flow::Summaries.new(flow_host.flows.find(params[:id]))
|
|
24
|
+
summaries.describe(details[:summary]) unless details[:summary].nil?
|
|
25
|
+
summaries.start_on(Page.find_by(id: details[:intro_page_id])) unless details[:intro_page_id].nil?
|
|
26
|
+
summaries.finish_on(Page.find_by(id: details[:summary_page_id])) unless details[:summary_page_id].nil?
|
|
27
|
+
summaries.show_answer_values(details[:shows_answer_values] == "1") unless details[:shows_answer_values].nil?
|
|
28
|
+
summaries.ask_on_one_page(details[:asks_on_one_page] == "1") unless details[:asks_on_one_page].nil?
|
|
29
|
+
record_outputs(summaries, details[:outputs]) if details[:outputs]
|
|
13
30
|
|
|
14
31
|
super
|
|
32
|
+
rescue Summary::UnknownOutputType => refused
|
|
33
|
+
redirect_to easy_flow.edit_manage_flow_path(params[:id]), alert: refused.message
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def scored_by_category?(outputs)
|
|
39
|
+
outputs.any? { |output| %w[grouped lowest].include?(output["type"]) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def questions_of(flow)
|
|
43
|
+
Array(flow.definition.to_h["nodes"]).select { |node| node["type"] == "question" }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def record_outputs(summaries, submitted)
|
|
47
|
+
edited = Flow::OutputForm.edited(Array(summaries.document.to_h["outputs"]), submitted.permit!.to_h)
|
|
48
|
+
summaries.record(summaries.document.to_h.merge("outputs" => edited))
|
|
15
49
|
end
|
|
16
50
|
end
|
|
17
51
|
end
|
|
@@ -12,7 +12,7 @@ module Alembic
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def block_content(block)
|
|
15
|
-
render_to_string(partial: "alembic/manage/pages/block_content", locals: { block: block }, formats: [ :html ])
|
|
15
|
+
render_to_string(partial: "alembic/manage/pages/block_content", locals: { block: block, page: block_layout_record }, formats: [ :html ])
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -27,6 +27,13 @@ module Alembic
|
|
|
27
27
|
redirect_to manage_page_path(page)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def preview
|
|
31
|
+
page = Page.find(params[:id])
|
|
32
|
+
session[:alembic_preview] = session.fetch(:alembic_preview, {}).merge(page.id.to_s => params[:choice].presence)
|
|
33
|
+
|
|
34
|
+
redirect_to manage_page_path(page)
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
private
|
|
31
38
|
|
|
32
39
|
def payload(page)
|
|
@@ -5,7 +5,11 @@ module Alembic
|
|
|
5
5
|
included do
|
|
6
6
|
layout -> { Alembic.layout }
|
|
7
7
|
helper ApplicationHelper
|
|
8
|
-
|
|
8
|
+
helper PagesHelper
|
|
9
|
+
helper ResultBlocksHelper
|
|
10
|
+
helper ContentBlocksHelper
|
|
11
|
+
helper KsBlocks::LayoutHelper
|
|
12
|
+
helper_method :flow_summary, :first_step_path, :summary_values, :intro_values
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
private
|
|
@@ -25,7 +29,36 @@ module Alembic
|
|
|
25
29
|
def finished(answers, finished_run)
|
|
26
30
|
@outputs = outputs_of(answers.transform_keys(&:to_s))
|
|
27
31
|
Flow::Summaries.new(flow).pin(finished_run) if finished_run && run.nil?
|
|
28
|
-
|
|
32
|
+
@summary_page = Flow::Summaries.new(run&.flow || flow).summary_page
|
|
33
|
+
render template: @summary_page&.live_version ? "alembic/flows/summary_page" : "alembic/flows/complete"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def intro_values
|
|
37
|
+
starts_a_run = @flow.each_step? && !previewing?
|
|
38
|
+
{ "title" => @flow.title.presence || @flow.slug, "summary" => flow_summary(@flow),
|
|
39
|
+
"question_count" => question_count, "starts_a_run" => starts_a_run,
|
|
40
|
+
"start_path" => starts_a_run ? alembic.flow_runs_path(@flow.slug) : first_step_path(@flow.slug) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def question_count
|
|
44
|
+
Array(@flow.live_definition.to_h["nodes"]).count { |node| node["type"] == "question" }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def summary_values
|
|
48
|
+
@outputs.to_h { |output| [ output.id, output.value ] }
|
|
49
|
+
.merge("slug" => @guide.slug, "note" => result_note, "answers" => answers_given)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def answers_given
|
|
53
|
+
@answered.map { |step, value| [ @guide.question_text(step), @guide.choice_label(step, value) ] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def result_note
|
|
57
|
+
finished_flow = run&.flow || flow
|
|
58
|
+
return Alembic.lead_note.call(finished_flow.title, @outputs.to_h { |output| [ output.id, output.value ] }) if Alembic.lead_note
|
|
59
|
+
|
|
60
|
+
[ finished_flow.title, *@outputs.map { |output| "#{output.label}: #{helpers.alembic_output_lines(output.value).join(', ')}" } ]
|
|
61
|
+
.compact_blank.join(". ")
|
|
29
62
|
end
|
|
30
63
|
|
|
31
64
|
def outputs_of(state)
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
module Alembic
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
def alembic_rules_text(rules)
|
|
4
|
+
Array(rules).map do |rule|
|
|
5
|
+
conditions = Array(rule["when"]).map do |condition|
|
|
6
|
+
condition.key?("is_not") ? "#{condition['step']} is not #{condition['is_not']}" : "#{condition['step']} is #{condition['is']}"
|
|
7
|
+
end
|
|
8
|
+
"#{rule['entry']}: #{conditions.join(', ')}".strip
|
|
9
|
+
end.join("\n")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def alembic_entry_texts(entry)
|
|
13
|
+
{ "facts_text" => Array(entry["facts"]).map { |pair| pair.join(" | ") }.join("\n"),
|
|
14
|
+
"sections_text" => Array(entry["sections"]).map { |section| section.values_at("heading", "text", "note").compact.join(" | ") }.join("\n"),
|
|
15
|
+
"steps_text" => Array(entry["steps"]).map { |step| "#{step['title']}\n#{step['code']}" }.join("\n---\n") }
|
|
16
|
+
end
|
|
17
|
+
|
|
3
18
|
def alembic_output_lines(value)
|
|
4
19
|
case value
|
|
5
|
-
when Hash then value.map { |name, share| "#{name}: #{share}" }
|
|
6
|
-
when Array then value
|
|
20
|
+
when Hash then value.key?("name") ? [ value["name"] ] : value.map { |name, share| "#{name}: #{share}" }
|
|
21
|
+
when Array then value.map { |item| item.is_a?(Hash) ? item["name"] : item }
|
|
7
22
|
else [ value ]
|
|
8
23
|
end
|
|
9
24
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module ContentBlocksHelper
|
|
3
|
+
def alembic_hero_block(headline: nil, kicker: nil, text: nil)
|
|
4
|
+
render "alembic/blocks/hero", headline: headline, kicker: kicker, text: text
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def alembic_facts_block(question_count: nil)
|
|
8
|
+
facts = yield.to_s.lines.map(&:strip).compact_blank.map do |line|
|
|
9
|
+
line.gsub("{question_count}", question_count.to_s).split("|", 2).map(&:strip)
|
|
10
|
+
end
|
|
11
|
+
render "alembic/blocks/facts", facts: facts
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
START_LOOK = "inline-flex items-center justify-center font-semibold rounded-lg bg-accent-600 text-white hover:bg-accent-500 px-6 py-3 cursor-pointer".freeze
|
|
15
|
+
|
|
16
|
+
def alembic_start_block(start_path:, label: "Start →", starts_a_run: false)
|
|
17
|
+
return button_to(label, start_path, method: :post, class: START_LOOK) if starts_a_run
|
|
18
|
+
|
|
19
|
+
link_to label, start_path, class: START_LOOK
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def alembic_code_block
|
|
23
|
+
render "alembic/blocks/code", code: yield.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def alembic_table_block
|
|
27
|
+
heading, *rows = yield.to_s.lines.map(&:strip).compact_blank.map { |line| line.split("|").map(&:strip) }
|
|
28
|
+
render "alembic/blocks/table", heading: heading.to_a, rows: rows
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -4,28 +4,29 @@ module Alembic
|
|
|
4
4
|
module PagesHelper
|
|
5
5
|
UNDRAWABLE = "This block cannot be drawn until its fields are filled in.".freeze
|
|
6
6
|
|
|
7
|
-
def drawn_page(page, being_edited: false)
|
|
7
|
+
def drawn_page(page, being_edited: false, values: {})
|
|
8
8
|
blocks = being_edited ? page.blocks : page.live_version&.blocks.to_a
|
|
9
|
-
drawn = blocks.to_h { |block| [ block["id"], drawn_block(block) ] }.compact
|
|
9
|
+
drawn = blocks.to_h { |block| [ block["id"], drawn_block(block, values: values) ] }.compact
|
|
10
10
|
|
|
11
11
|
block_layout(blocks.select { |block| drawn.key?(block["id"]) }, kind: :pages) do |block|
|
|
12
12
|
drawn.fetch(block["id"])
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def drawn_block(block)
|
|
16
|
+
def drawn_block(block, values: {})
|
|
17
17
|
component = Page::Drawing.of(block["type"])
|
|
18
18
|
return unless component
|
|
19
19
|
|
|
20
|
+
options = Page::Options.for(block, values: values)
|
|
20
21
|
body = Page::Options.body_for(block)
|
|
21
|
-
body.nil? ? send(component, **
|
|
22
|
+
body.nil? ? send(component, **options) : send(component, **options) { body }
|
|
22
23
|
rescue StandardError => undrawable
|
|
23
24
|
Rails.logger.error("Alembic could not draw the #{block['type']} block: #{undrawable.message}")
|
|
24
25
|
nil
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def shown_block(block)
|
|
28
|
-
drawn_block(block) || undrawn_block(block)
|
|
28
|
+
def shown_block(block, values: {})
|
|
29
|
+
drawn_block(block, values: values) || undrawn_block(block)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
private
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module ResultBlocksHelper
|
|
3
|
+
def alembic_score_block(value: nil, caption: nil, kicker: "Your result")
|
|
4
|
+
render "alembic/blocks/score", value: value, caption: caption, kicker: kicker
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def alembic_band_block(value: nil, score: nil)
|
|
8
|
+
render "alembic/blocks/band", band: value.to_h, tone: alembic_tone(score)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def alembic_categories_block(value: nil, heading: "Where you stand, area by area")
|
|
12
|
+
render "alembic/blocks/categories", shares: value.to_h, heading: heading
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def alembic_weakest_block(value: nil, heading: "Your biggest blind spots", cost_label: "What it likely costs you:")
|
|
16
|
+
render "alembic/blocks/weakest", categories: Array(value), heading: heading, cost_label: cost_label
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def alembic_answers_block(value: nil, heading: "What you told us")
|
|
20
|
+
render "alembic/blocks/answers", answers: Array(value), heading: heading
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def alembic_lead_block(slug:, note: nil, heading: "The next step", blurb: nil, button: "Send", placeholder: "you@example.com")
|
|
24
|
+
render "alembic/blocks/lead", address: Alembic.lead_address.call(slug), slug: slug, note: note,
|
|
25
|
+
heading: heading, blurb: blurb, button: button, placeholder: placeholder
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def alembic_default_result(output, outputs = [])
|
|
29
|
+
case output.type
|
|
30
|
+
when "percentage" then alembic_score_block(value: output.value, caption: output.label)
|
|
31
|
+
when "band" then alembic_band_block(value: output.value, score: outputs.find { |other| other.type == "percentage" }&.value)
|
|
32
|
+
when "grouped" then alembic_categories_block(value: output.value)
|
|
33
|
+
when "lowest" then alembic_weakest_block(value: output.value)
|
|
34
|
+
when "outcome" then alembic_outcome_block(value: output.value)
|
|
35
|
+
else ui_stat_card(label: output.label, value: alembic_output_lines(output.value).join(" · "))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def alembic_outcome_block(value: nil)
|
|
40
|
+
chosen = value.to_h["chosen"]
|
|
41
|
+
return "" if chosen.nil?
|
|
42
|
+
|
|
43
|
+
render "alembic/blocks/outcome_entry", entry: chosen, open: true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def alembic_outcomes_block(value: nil, heading: nil)
|
|
47
|
+
render "alembic/blocks/outcomes", entries: Array(value.to_h["entries"]), heading: heading
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def alembic_tone(percentage)
|
|
51
|
+
return "bg-red-500" if percentage.to_i < 40
|
|
52
|
+
return "bg-amber-500" if percentage.to_i < 70
|
|
53
|
+
|
|
54
|
+
"bg-emerald-500"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -4,6 +4,8 @@ module Alembic
|
|
|
4
4
|
self.table_name = "alembic_flow_definition_summaries"
|
|
5
5
|
|
|
6
6
|
belongs_to :flow, class_name: "EasyFlow::Definition"
|
|
7
|
+
belongs_to :summary_page, class_name: "Alembic::Page", optional: true
|
|
8
|
+
belongs_to :intro_page, class_name: "Alembic::Page", optional: true
|
|
7
9
|
end
|
|
8
10
|
end
|
|
9
11
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module OutputForm
|
|
4
|
+
def self.edited(current, submitted)
|
|
5
|
+
edited = submitted.sort_by { |index, _| index.to_i }.map do |index, output|
|
|
6
|
+
current.fetch(index.to_i, {}).merge(output.except("rules_text")).merge(listed_bands(output)).merge(counted(output))
|
|
7
|
+
.merge(ruled(output)).merge(listed_entries(output))
|
|
8
|
+
end
|
|
9
|
+
edited.select { |output| output["id"].present? }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.listed_entries(output)
|
|
13
|
+
return {} unless output["entries"].is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
entries = output["entries"].sort_by { |position, _| position.to_i }.map(&:last).select { |entry| entry["key"].present? }
|
|
16
|
+
{ "entries" => entries.map { |entry| entry_from(entry) } }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.entry_from(entry)
|
|
20
|
+
entry.except("facts_text", "sections_text", "steps_text")
|
|
21
|
+
.merge("facts" => pairs(entry["facts_text"]), "sections" => sections(entry["sections_text"]), "steps" => steps(entry["steps_text"]))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.steps(text)
|
|
25
|
+
text.to_s.split(/^---\s*$/).map(&:strip).compact_blank.map do |block|
|
|
26
|
+
title, code = block.split("\n", 2)
|
|
27
|
+
{ "title" => title.strip, "code" => code.to_s.strip }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.sections(text)
|
|
32
|
+
text.to_s.lines.map(&:strip).compact_blank.map do |line|
|
|
33
|
+
heading, body, note = line.split("|", 3).map(&:strip)
|
|
34
|
+
{ "heading" => heading, "text" => body, "note" => note }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.pairs(text)
|
|
39
|
+
text.to_s.lines.map(&:strip).compact_blank.map { |line| line.split("|", 2).map(&:strip) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.ruled(output)
|
|
43
|
+
return {} unless output.key?("rules_text")
|
|
44
|
+
|
|
45
|
+
rules = output["rules_text"].to_s.lines.map(&:strip).compact_blank.map do |line|
|
|
46
|
+
entry, conditions = line.split(":", 2)
|
|
47
|
+
{ "entry" => entry.strip, "when" => conditions.to_s.split(",").map(&:strip).compact_blank.map { |condition| condition_from(condition) } }
|
|
48
|
+
end
|
|
49
|
+
{ "rules" => rules }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.condition_from(text)
|
|
53
|
+
step, answer = text.split(/\s+is not\s+/, 2)
|
|
54
|
+
return { "step" => step, "is_not" => answer } if answer
|
|
55
|
+
|
|
56
|
+
step, answer = text.split(/\s+is\s+/, 2)
|
|
57
|
+
{ "step" => step, "is" => answer }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.counted(output)
|
|
61
|
+
output.key?("count") ? { "count" => output["count"].presence&.to_i } : {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.listed_bands(output)
|
|
65
|
+
return {} unless output["bands"].is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
named = output["bands"].sort_by { |position, _| position.to_i }.map(&:last).select { |band| band["name"].present? }
|
|
68
|
+
{ "bands" => named.map { |band| band.merge("ceiling" => band["ceiling"].presence&.to_i) } }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private_class_method :counted, :listed_bands, :ruled, :condition_from, :listed_entries, :pairs, :entry_from, :sections, :steps
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
module PreviewValues
|
|
4
|
+
def self.values(page, choice)
|
|
5
|
+
flow = flow_for(page)
|
|
6
|
+
return {} if flow.nil?
|
|
7
|
+
|
|
8
|
+
summaries = Summaries.new(flow)
|
|
9
|
+
state = finished_runs(flow).find { |run| run.id.to_s == choice.to_s }&.recorded || sample_state(flow)
|
|
10
|
+
introduced(flow, summaries).merge(summaries.of(state).to_h { |result| [ result.id, result.value ] })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.finished_runs(flow)
|
|
14
|
+
EasyFlow::Run.where(flow: flow).order(created_at: :desc).limit(20).select { |run| run.next_step(run.recorded).nil? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.introduced(flow, summaries)
|
|
18
|
+
questions = Array(flow.live_definition.to_h["nodes"]).count { |node| node["type"] == "question" }
|
|
19
|
+
{ "title" => flow.title.presence || flow.slug, "summary" => summaries.text, "question_count" => questions,
|
|
20
|
+
"slug" => flow.slug, "start_path" => "#", "starts_a_run" => false }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.choices(page)
|
|
24
|
+
flow = flow_for(page)
|
|
25
|
+
return [] if flow.nil?
|
|
26
|
+
|
|
27
|
+
finished_runs(flow).map { |run| [ "Run of #{I18n.l(run.created_at, format: :short)}", run.id.to_s ] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.flow_for(page)
|
|
31
|
+
DefinitionSummary.where(summary_page: page).or(DefinitionSummary.where(intro_page: page)).first&.flow
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.sample_state(flow)
|
|
35
|
+
questions = Array(flow.live_definition.to_h["nodes"]).select { |node| node["type"] == "question" }
|
|
36
|
+
questions.each_with_index.filter_map do |question, index|
|
|
37
|
+
answers = EasyFlow::Steps::Question.answers_of(question)
|
|
38
|
+
next if answers.empty?
|
|
39
|
+
|
|
40
|
+
[ question["id"], answers[index % answers.size].then { |answer| answer.is_a?(Hash) ? answer["value"] : answer } ]
|
|
41
|
+
end.to_h
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :flow_for, :introduced, :finished_runs
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -14,6 +14,7 @@ module Alembic
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def record(payload)
|
|
17
|
+
Array(payload.to_h["outputs"]).each { |output| Summary.registry.fetch(output["type"]) if output["type"].present? }
|
|
17
18
|
versions.create!(number: next_number, summary: payload)
|
|
18
19
|
.tap { |version| details.update!(summary_cursor: version.number) }
|
|
19
20
|
end
|
|
@@ -34,6 +35,44 @@ module Alembic
|
|
|
34
35
|
details.update!(summary: text)
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def summary_page
|
|
39
|
+
details.summary_page
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def shows_answer_values?
|
|
43
|
+
details.shows_answer_values
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show_answer_values(shown)
|
|
47
|
+
details.update!(shows_answer_values: shown)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def asks_on_one_page?
|
|
51
|
+
details.asks_on_one_page
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ask_on_one_page(asked)
|
|
55
|
+
details.update!(asks_on_one_page: asked && !branches?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def branches?
|
|
59
|
+
Array(@flow.definition.to_h["nodes"]).any? do |node|
|
|
60
|
+
EasyFlow.registry.registered?(node["type"]) && EasyFlow.registry.fetch(node["type"]).routes?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def intro_page
|
|
65
|
+
details.intro_page
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def start_on(page)
|
|
69
|
+
details.update!(intro_page: page)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def finish_on(page)
|
|
73
|
+
details.update!(summary_page: page)
|
|
74
|
+
end
|
|
75
|
+
|
|
37
76
|
def pin(run)
|
|
38
77
|
RunSummary.create!(run: run, summary_version: current_version) if current_version
|
|
39
78
|
end
|
|
@@ -15,7 +15,7 @@ module Alembic
|
|
|
15
15
|
def self.covering(config, number)
|
|
16
16
|
ordered = Array(config["bands"]).sort_by { |band| band["ceiling"] || Float::INFINITY }
|
|
17
17
|
|
|
18
|
-
ordered.find { |band| band["ceiling"].nil? || number < band["ceiling"] }&.
|
|
18
|
+
ordered.find { |band| band["ceiling"].nil? || number < band["ceiling"] }&.slice("name", "description")
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -4,7 +4,7 @@ module Alembic
|
|
|
4
4
|
def self.output_type
|
|
5
5
|
Summary::OutputType.define(:lowest) do
|
|
6
6
|
label "Weakest"
|
|
7
|
-
compute { |config, _run, so_far| Lowest.of(so_far[config["of"]], config["count"]) }
|
|
7
|
+
compute { |config, _run, so_far| Lowest.of(so_far[config["of"]], config["count"], config["copy"]) }
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -12,8 +12,10 @@ module Alembic
|
|
|
12
12
|
registry.register(output_type)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.of(shares, count)
|
|
16
|
-
shares.to_h.sort_by { |_name, share| share }.first(count.presence&.to_i || 1).map(&:first)
|
|
15
|
+
def self.of(shares, count, copy = nil)
|
|
16
|
+
weakest = shares.to_h.sort_by { |_name, share| share }.first(count.presence&.to_i || 1).map(&:first)
|
|
17
|
+
|
|
18
|
+
weakest.map { |name| { "name" => name, **copy.to_h.fetch(name, {}).slice("miss", "cost") } }
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Outcome
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:outcome) do
|
|
6
|
+
label "Outcome"
|
|
7
|
+
compute { |config, run, _so_far| Outcome.placed(config, run.state) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.placed(config, state)
|
|
16
|
+
entries = Array(config["entries"])
|
|
17
|
+
rule = Array(config["rules"]).find { |candidate| holds?(candidate, state) }
|
|
18
|
+
|
|
19
|
+
{ "chosen" => entries.find { |entry| entry["key"] == rule&.dig("entry") }, "entries" => entries }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.holds?(rule, state)
|
|
23
|
+
Array(rule["when"]).all? { |condition| met?(condition, state[condition["step"]].to_s) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.met?(condition, answer)
|
|
27
|
+
return answer != condition["is_not"].to_s if condition.key?("is_not")
|
|
28
|
+
|
|
29
|
+
answer == condition["is"].to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|