alembic 0.7.1 → 0.8.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/controllers/alembic/flows_controller.rb +4 -0
- data/app/controllers/concerns/alembic/summarizes.rb +14 -1
- data/app/helpers/alembic/application_helper.rb +7 -0
- data/app/models/alembic/flow/summaries.rb +10 -4
- data/app/models/alembic/flow/uploaded_question.rb +87 -0
- data/app/models/alembic/flow/uploaded_questions.rb +17 -0
- data/app/models/alembic/outputs/inner_total.rb +26 -0
- data/app/models/alembic/outputs/percentage.rb +3 -0
- data/app/models/alembic/outputs/weighted_sum.rb +23 -0
- data/app/models/alembic/summary/run.rb +2 -2
- data/app/views/alembic/flows/complete.html.erb +6 -0
- data/app/views/alembic/flows/summary_page.html.erb +5 -0
- data/app/views/alembic/manage/flows/_outputs.html.erb +6 -0
- data/app/views/alembic/manage/flows/edit.html.erb +6 -0
- data/lib/alembic/engine.rb +5 -0
- data/lib/alembic/question_importer.rb +17 -0
- data/lib/alembic/version.rb +1 -1
- data/lib/alembic.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9fe70ef3945d5a70f24110f819a17db43ee9f54ebf70e81631e76e96fe4e7a2
|
|
4
|
+
data.tar.gz: eb5c70109254d9e121b2fbe5b8bbf6ce7988e53deaf322a9a904ee556c6ede6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98520320480c38a5a72a8d11b11f0510b70352c8a6a9765c732842dae8075a6f843c419f29fe9c3308a5b9b8230fe8a20840274d4e5b866aa741ea414b2cbab0
|
|
7
|
+
data.tar.gz: 131062d6d22160f9ab6b3688d33f43120b783410d9836c2c03d09d6617b8efe47c7656896eca9bb0537151bf801cf1971564b7543afc279994071b40965636d3
|
|
@@ -9,7 +9,8 @@ module Alembic
|
|
|
9
9
|
helper ResultBlocksHelper
|
|
10
10
|
helper ContentBlocksHelper
|
|
11
11
|
helper KsBlocks::LayoutHelper
|
|
12
|
-
|
|
12
|
+
after_action :keep_ended_inner_run, only: :step
|
|
13
|
+
helper_method :flow_summary, :first_step_path, :summary_values, :intro_values, :start_over_path, :finish_link, :visited_flow_address
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
private
|
|
@@ -26,6 +27,8 @@ module Alembic
|
|
|
26
27
|
flow_start_path(visited_flow_address)
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
def finish_link; end
|
|
31
|
+
|
|
29
32
|
def visited_flow_address
|
|
30
33
|
flow_address((run&.flow || flow).slug)
|
|
31
34
|
end
|
|
@@ -40,6 +43,16 @@ module Alembic
|
|
|
40
43
|
super.tap { |run| Flow::Summaries.new(flow).pin(run) }
|
|
41
44
|
end
|
|
42
45
|
|
|
46
|
+
def admitted_run
|
|
47
|
+
super.tap { |found| Flow::Summaries.new(found.flow).pin_inner(found) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def keep_ended_inner_run
|
|
51
|
+
return unless run&.parent_run && run.ended_at_end?
|
|
52
|
+
|
|
53
|
+
Flow::Summaries.new(run.flow).keep(run, run.walked(run.recorded).transform_keys(&:to_s))
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
def finished(answers, finished_run)
|
|
44
57
|
@outputs = outputs_of(answers.transform_keys(&:to_s))
|
|
45
58
|
Flow::Summaries.new(flow).pin(finished_run) if finished_run && run.nil?
|
|
@@ -15,6 +15,13 @@ module Alembic
|
|
|
15
15
|
"steps_text" => Array(entry["steps"]).map { |step| "#{step['title']}\n#{step['code']}" }.join("\n---\n") }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def alembic_question_upload_path(flow)
|
|
19
|
+
registration = defined?(Ixport) && Ixport.registrations.values.find { |found| found.importer == "Alembic::QuestionImporter" }
|
|
20
|
+
return unless registration && respond_to?(:ixport)
|
|
21
|
+
|
|
22
|
+
ixport.new_import_path(key: registration.key, target: flow.to_global_id)
|
|
23
|
+
end
|
|
24
|
+
|
|
18
25
|
def alembic_output_lines(value)
|
|
19
26
|
case value
|
|
20
27
|
when Hash then value.key?("name") ? [ value["name"] ] : value.map { |name, share| "#{name}: #{share}" }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Alembic
|
|
2
2
|
module Flow
|
|
3
3
|
class Summaries
|
|
4
|
+
SCORES = %w[weighted_sum inner_total].freeze
|
|
5
|
+
|
|
4
6
|
def initialize(flow)
|
|
5
7
|
@flow = flow
|
|
6
8
|
end
|
|
@@ -77,6 +79,10 @@ module Alembic
|
|
|
77
79
|
RunSummary.create!(run: run, summary_version: current_version) if current_version
|
|
78
80
|
end
|
|
79
81
|
|
|
82
|
+
def pin_inner(run)
|
|
83
|
+
pin(run) if run.parent_run && pinned_to(run).nil?
|
|
84
|
+
end
|
|
85
|
+
|
|
80
86
|
def pinned_to(run)
|
|
81
87
|
RunSummary.find_by(run: run)&.summary_version
|
|
82
88
|
end
|
|
@@ -96,15 +102,15 @@ module Alembic
|
|
|
96
102
|
pinned = pinned_to(run)&.summary
|
|
97
103
|
return [] if pinned.blank?
|
|
98
104
|
|
|
99
|
-
Summary::Report.new(pinned).results(Summary::Run.new(state: state, steps: run.pinned_steps))
|
|
105
|
+
Summary::Report.new(pinned).results(Summary::Run.new(state: state, steps: run.pinned_steps, run: run))
|
|
100
106
|
end
|
|
101
107
|
|
|
102
|
-
private
|
|
103
|
-
|
|
104
108
|
def score_of(run, state)
|
|
105
|
-
of_run(run, state).find { |result| result.type
|
|
109
|
+
of_run(run, state).find { |result| SCORES.include?(result.type) }&.value
|
|
106
110
|
end
|
|
107
111
|
|
|
112
|
+
private
|
|
113
|
+
|
|
108
114
|
def seconds_taken_by(run)
|
|
109
115
|
return unless run.flow.each_step?
|
|
110
116
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class UploadedQuestion
|
|
4
|
+
include ActiveModel::Model
|
|
5
|
+
include ActiveModel::Attributes
|
|
6
|
+
|
|
7
|
+
attr_accessor :flow
|
|
8
|
+
|
|
9
|
+
attribute :type, :string
|
|
10
|
+
attribute :question, :string
|
|
11
|
+
attribute :category, :string
|
|
12
|
+
attribute :right_answer, :string
|
|
13
|
+
ANSWER_FIELDS = (1..6).map { |number| :"answer_#{number}" }
|
|
14
|
+
ANSWER_FIELDS.each { |field| attribute field, :string }
|
|
15
|
+
|
|
16
|
+
KINDS = %w[truefalse multiplechoice].freeze
|
|
17
|
+
|
|
18
|
+
validates :question, presence: true
|
|
19
|
+
validate :kind_is_offered
|
|
20
|
+
validate :choices_are_offered, if: -> { kind == "multiplechoice" }
|
|
21
|
+
validate :true_or_false_is_right, if: -> { kind == "truefalse" }
|
|
22
|
+
|
|
23
|
+
def save!
|
|
24
|
+
before = document.to_h
|
|
25
|
+
flow.edit_history.record(appended, :added, [ step["id"] ], before)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def choices_are_offered
|
|
31
|
+
errors.add(:answers, "must be at least two for a multiple choice question") if filled_answers.size < 2
|
|
32
|
+
errors.add(:right_answer, "must be the number of one of its answers") if answers.none? { |answer| answer["weight"] == 1 }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def true_or_false_is_right
|
|
36
|
+
errors.add(:right_answer, "must be true or false") if answers.none? { |answer| answer["weight"] == 1 }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def kind_is_offered
|
|
40
|
+
errors.add(:type, "must be true/false or multiple choice") unless KINDS.include?(kind)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def document
|
|
44
|
+
EasyFlow::Document.new(flow.document || flow.definition || {})
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def appended
|
|
48
|
+
leaving = document.edges_from(last_question).first
|
|
49
|
+
return document.insert(step, on: [ last_question, leaving.to ]) if leaving
|
|
50
|
+
|
|
51
|
+
document.add(step).connect(from: last_question, to: step["id"])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def last_question
|
|
55
|
+
reached = document.reachable
|
|
56
|
+
reached.reverse.find { |id| document.node(id)&.type == "question" } || reached.last
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def step
|
|
60
|
+
@step ||= { "id" => next_id, "type" => "question", "question" => question, "category" => category.presence, "answers" => answers }.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def answers
|
|
64
|
+
return true_false_answers if kind == "truefalse"
|
|
65
|
+
|
|
66
|
+
filled_answers.map { |number, text| { "value" => text, "weight" => number.to_s == right_answer.to_s.strip ? 1 : 0 } }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def true_false_answers
|
|
70
|
+
%w[True False].map { |text| { "value" => text, "weight" => text.casecmp?(right_answer.to_s.strip) ? 1 : 0 } }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def kind
|
|
74
|
+
type.to_s.downcase.gsub(/[^a-z]/, "")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def filled_answers
|
|
78
|
+
ANSWER_FIELDS.each_with_index.filter_map { |field, index| [ index + 1, public_send(field).strip ] if public_send(field).present? }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def next_id
|
|
82
|
+
taken = document.nodes.map(&:id)
|
|
83
|
+
(1..).lazy.map { |number| "question_#{number}" }.find { |id| !taken.include?(id) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module InnerTotal
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:inner_total) do
|
|
6
|
+
label "Total score"
|
|
7
|
+
compute { |_config, run, _so_far| InnerTotal.total(run.run) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.total(parent)
|
|
16
|
+
return 0 if parent.nil?
|
|
17
|
+
|
|
18
|
+
parent.inner_runs.select(&:ended_at_end?).sum { |inner| score_of(inner).to_i }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.score_of(inner)
|
|
22
|
+
Flow::Summaries.new(inner.flow).score_of(inner, inner.walked(inner.recorded).transform_keys(&:to_s))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -24,6 +24,9 @@ module Alembic
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def self.most(step)
|
|
27
|
+
return 1 if step["type"] == "multiple_select"
|
|
28
|
+
return WeightedSum.weight_of(step, Array(step["accepted_answers"]).first&.dig("value")) if step["type"] == "fill_in_the_blank"
|
|
29
|
+
|
|
27
30
|
EasyFlow::Steps::Question.answers_of(step).filter_map { |option| option["weight"].to_i if option.is_a?(Hash) }.max.to_i
|
|
28
31
|
end
|
|
29
32
|
end
|
|
@@ -17,10 +17,33 @@ module Alembic
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.weight_of(step, value)
|
|
20
|
+
return picked_weight(step, value) if step["type"] == "multiple_select"
|
|
21
|
+
return typed_weight(step, value) if step["type"] == "fill_in_the_blank"
|
|
22
|
+
|
|
23
|
+
answer_weight(step, value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.answer_weight(step, value)
|
|
20
27
|
chosen = EasyFlow::Steps::Question.answers_of(step).find { |option| option.is_a?(Hash) && option["value"] == value }
|
|
21
28
|
|
|
22
29
|
chosen&.fetch("weight", nil).to_i
|
|
23
30
|
end
|
|
31
|
+
|
|
32
|
+
def self.picked_weight(step, values)
|
|
33
|
+
Array(values).sort == right_answers(step).sort ? 1 : 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.typed_weight(step, text)
|
|
37
|
+
Array(step["accepted_answers"]).any? { |accepted| comparable(accepted["value"]) == comparable(text) } ? 1 : 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.comparable(text)
|
|
41
|
+
text.to_s.strip.downcase
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.right_answers(step)
|
|
45
|
+
EasyFlow::Steps::Question.answers_of(step).filter_map { |option| option["value"] if option["weight"].to_i.positive? }
|
|
46
|
+
end
|
|
24
47
|
end
|
|
25
48
|
end
|
|
26
49
|
end
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
<% end %>
|
|
22
22
|
<% end %>
|
|
23
23
|
|
|
24
|
+
<% if (link = finish_link) %>
|
|
25
|
+
<%= ui_section do %>
|
|
26
|
+
<%= ui_button(label: link[:label], href: link[:url]) %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
24
30
|
<% if Alembic.lead_address %>
|
|
25
31
|
<%= ui_section do %>
|
|
26
32
|
<%= alembic_lead_block(slug: summary_values["slug"], note: summary_values["note"]) %>
|
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
<section class="py-20 md:py-28">
|
|
6
6
|
<div class="max-w-2xl mx-auto px-6">
|
|
7
7
|
<%= drawn_page(@summary_page, values: summary_values) %>
|
|
8
|
+
<% if (link = finish_link) %>
|
|
9
|
+
<div class="mt-10">
|
|
10
|
+
<%= ui_button(label: link[:label], href: link[:url]) %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
8
13
|
</div>
|
|
9
14
|
</section>
|
|
@@ -58,4 +58,10 @@
|
|
|
58
58
|
<%= hidden_field_tag "#{field}[type]", "outcome", id: nil %>
|
|
59
59
|
<%= ui_input(name: "#{field}[id]", placeholder: "The new outcome's id") %>
|
|
60
60
|
</div>
|
|
61
|
+
<% field = "flow[outputs][#{outputs.size + 1}]" %>
|
|
62
|
+
<div data-new-inner-total class="mt-6">
|
|
63
|
+
<label class="ks-label mb-1">Add a total of every inner run's score</label>
|
|
64
|
+
<%= hidden_field_tag "#{field}[type]", "inner_total", id: nil %>
|
|
65
|
+
<%= ui_input(name: "#{field}[id]", placeholder: "The new total's id") %>
|
|
66
|
+
</div>
|
|
61
67
|
<% end %>
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
<%= ui_form_page(title: "Edit details", back_url: easy_flow.manage_flow_path(@flow)) %>
|
|
7
7
|
|
|
8
|
+
<% if (upload_path = alembic_question_upload_path(@flow)) %>
|
|
9
|
+
<%= ui_section(title: "Questions from a file") do %>
|
|
10
|
+
<%= link_to "Upload questions", upload_path, class: "text-sm text-(color:--ks-color-link) hover:text-(color:--ks-color-link-hover) dark:text-(color:--ks-color-link-dark) dark:hover:text-(color:--ks-color-link-hover-dark)" %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
8
14
|
<%= ui_form(action: easy_flow.manage_flow_path(@flow), method: :patch) do %>
|
|
9
15
|
<%= ui_section(title: "How this is introduced") do %>
|
|
10
16
|
<label class="ks-label mb-1">Title</label>
|
data/lib/alembic/engine.rb
CHANGED
|
@@ -29,9 +29,14 @@ module Alembic
|
|
|
29
29
|
Alembic::Outputs::Tally.register
|
|
30
30
|
Alembic::Outputs::Band.register
|
|
31
31
|
Alembic::Outputs::Outcome.register
|
|
32
|
+
Alembic::Outputs::InnerTotal.register
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
initializer "alembic.question_importer" do |app|
|
|
37
|
+
app.config.to_prepare { require "alembic/question_importer" if defined?(Ixport::Importer) }
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
initializer "alembic.result_blocks" do |app|
|
|
36
41
|
app.config.to_prepare do
|
|
37
42
|
Alembic::Page::ContentBlocks.register
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
class QuestionImporter < Ixport::Importer
|
|
3
|
+
field :type, label: "Type"
|
|
4
|
+
field :question, label: "Question"
|
|
5
|
+
field :category, label: "Category"
|
|
6
|
+
Flow::UploadedQuestion::ANSWER_FIELDS.each.with_index(1) { |key, number| field key, label: "Answer #{number}" }
|
|
7
|
+
field :right_answer, label: "Right answer"
|
|
8
|
+
|
|
9
|
+
def records
|
|
10
|
+
Flow::UploadedQuestions.new(target)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.allows?(account, quiz)
|
|
14
|
+
quiz.is_a?(EasyFlow::Definition) && quiz.host == FLOW_HOST.to_s && (Alembic.owner_method.nil? || quiz.owner == account)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/alembic/version.rb
CHANGED
data/lib/alembic.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Alembic
|
|
|
13
13
|
visitor_authorization_method refusal_method owner_method].freeze
|
|
14
14
|
|
|
15
15
|
class << self
|
|
16
|
-
attr_accessor :lead_address, :lead_fields_partial, :lead_note, :start_over_address
|
|
16
|
+
attr_accessor :lead_address, :lead_fields_partial, :lead_note, :start_over_address, :finish_link
|
|
17
17
|
attr_reader :admin_authentication_method, :visitor_authorization_method, :refusal_method, :owner_method
|
|
18
18
|
|
|
19
19
|
FLOW_HOST_SETTINGS.each do |name|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: alembic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -57,14 +57,14 @@ dependencies:
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.
|
|
60
|
+
version: '0.27'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0.
|
|
67
|
+
version: '0.27'
|
|
68
68
|
description: Alembic builds and runs data-driven interactive diagnostics — scored
|
|
69
69
|
assessments and branching guides — that place a visitor on an outcome and capture
|
|
70
70
|
the result. A runtime engine plus a UI builder.
|
|
@@ -107,8 +107,11 @@ files:
|
|
|
107
107
|
- app/models/alembic/flow/run_summary.rb
|
|
108
108
|
- app/models/alembic/flow/summaries.rb
|
|
109
109
|
- app/models/alembic/flow/summary_version.rb
|
|
110
|
+
- app/models/alembic/flow/uploaded_question.rb
|
|
111
|
+
- app/models/alembic/flow/uploaded_questions.rb
|
|
110
112
|
- app/models/alembic/outputs/band.rb
|
|
111
113
|
- app/models/alembic/outputs/grouped.rb
|
|
114
|
+
- app/models/alembic/outputs/inner_total.rb
|
|
112
115
|
- app/models/alembic/outputs/lowest.rb
|
|
113
116
|
- app/models/alembic/outputs/outcome.rb
|
|
114
117
|
- app/models/alembic/outputs/percentage.rb
|
|
@@ -215,6 +218,7 @@ files:
|
|
|
215
218
|
- lib/alembic.rb
|
|
216
219
|
- lib/alembic/engine.rb
|
|
217
220
|
- lib/alembic/leftover_stylesheet.rb
|
|
221
|
+
- lib/alembic/question_importer.rb
|
|
218
222
|
- lib/alembic/version.rb
|
|
219
223
|
- lib/tasks/alembic_tasks.rake
|
|
220
224
|
homepage: https://github.com/tylercschneider/alembic
|