defmodule Glific.CSV.Flow do
@moduledoc """
Given a CSV model, and a tracking shortcode, generate the json flow for the CSV
incorporating the UUID's used in previous conversions. Store the latest UUID mapping
back in the database
"""
## we need to put the default height and default width based on the content
@default_height 300
@default_offset 100
@default_width 200
alias Glific.{
CSV.Menu,
CSV.Template,
Flows.FlowLabel
}
@doc """
Given a menu + content structure, generate the flow for it that matches flow editor input
"""
@spec gen_flow(Menu.t(), non_neg_integer, Keyword.t()) :: map()
def gen_flow(root, organization_id, opts \\ []) do
json_map = %{
name: "LAHI Grade 9",
expire_after_minutes: 10_080,
spec_version: "13.1.0",
type: "messaging",
uuid: root.uuids.root,
vars: [root.uuids.root],
language: "base",
nodes: [],
localization: %{},
_ui: %{
nodes: %{}
},
organization_id: organization_id,
opts: opts
}
# first generate the nodes and localization for this node
# then do it recursively for each of its menu items
json_map = gen_flow_helper(json_map, root)
json_map
|> Map.put(:nodes, Enum.reverse(json_map.nodes))
|> Glific.delete_multiple([:organization_id, :opts])
end
@spec gen_flow_helper(map(), Menu.t()) :: map()
defp gen_flow_helper(json_map, node) do
if Enum.empty?(node.sub_menus) do
gen_flow_content(json_map, node)
else
gen_flow_menu(json_map, node)
end
end
defp add_label(actions, %{label: nil}, _organization_id), do: actions
defp add_label(actions, %{label: name}, organization_id) do
{:ok, label} =
FlowLabel.get_or_create_flow_label(%{name: name, organization_id: organization_id})
[
%{
labels: [
%{
name: name,
uuid: label.uuid
}
],
type: "add_input_labels",
uuid: Ecto.UUID.generate()
}
| actions
]
end
# this is a menu node
# generate the message and the localization
# call gen_flow_helper on each sub_menu
@spec gen_flow_menu(map(), Menu.t()) :: map()
defp gen_flow_menu(json_map, node) do
actions =
[
%{
uuid: node.uuids.action,
quick_replies: [],
attachments: [],
text: menu_content(node.content["en"], "en", node, json_map),
type: "send_msg"
}
]
|> add_label(node, json_map.organization_id)
node_json = %{
uuid: node.uuids.node,
exits: [
%{
uuid: node.uuids.exit,
destination_uuid: node.uuids.router
}
],
actions: actions
}
menu_content =
node.content["en"]
|> indexed_content(node, json_map)
exits = get_exits(menu_content, get_destination_uuids(node), node.uuids.node)
cases = get_cases(menu_content)
{categories, default_category_uuid} = get_categories(menu_content, exits, cases)
router_json = %{
uuid: node.uuids.router,
actions: [],
exits: Map.values(exits),
router: %{
cases: Map.values(cases),
wait: %{type: "msg"},
operand: "@input.text",
categories: Map.values(categories),
default_category_uuid: default_category_uuid,
type: "switch"
}
}
json_map =
json_map
|> Map.update!(:nodes, fn n -> [router_json, node_json | n] end)
|> add_localization(node, :menu)
|> add_ui(node, :menu)
# now go thru all the sub_menu and call json_map for each of them
Enum.reduce(
node.sub_menus,
json_map,
fn menu, acc -> gen_flow_helper(acc, menu) end
)
end
defp add_ui(json_map, node, :content) do
nodes =
json_map._ui.nodes
|> Map.put(
node.uuids.node,
%{
position: %{
top: node.level * @default_height,
left: node.position * @default_width
},
type: "execute_actions"
}
)
put_in(json_map, [:_ui, :nodes], nodes)
end
defp add_ui(json_map, node, :menu) do
json_map = add_ui(json_map, node, :content)
nodes =
json_map._ui.nodes
|> Map.put(
node.uuids.router,
%{
position: %{
top: node.level * @default_height,
left: node.position * @default_width + @default_offset
},
config: %{
cases: %{}
},
type: "wait_for_response"
}
)
put_in(json_map, [:_ui, :nodes], nodes)
end
defp add_back_main_uuids(acc, node),
do:
acc
|> Map.put(8, node.uuids.root)
|> Map.put(9, node.uuids.parent)
defp get_destination_uuids(node) do
# collect all the destination uuids from the sub_menu
node.sub_menus
|> Enum.with_index(1)
|> Enum.reduce(
%{},
fn {s, idx}, acc -> Map.put(acc, idx, s.uuids.node) end
)
# add the back and main menu uuids
|> add_back_main_uuids(node)
end
defp indexed_content(content, node, json_map) do
content
|> Map.values()
|> Enum.with_index(1)
|> add_back_case(node, Keyword.get(json_map.opts, :back_menu_item, false))
|> add_main_case(node, Keyword.get(json_map.opts, :main_menu_item, false))
end
defp get_exits(menu_content, destination_uuids, node_uuid) do
exits =
menu_content
|> Enum.reduce(
%{},
fn {_str, idx}, acc ->
Map.put(
acc,
idx,
%{uuid: Ecto.UUID.generate(), destination_uuid: Map.get(destination_uuids, idx)}
)
end
)
# also add Other (and soon no response)
exits
|> Map.put(
10,
%{uuid: Ecto.UUID.generate(), destination_uuid: node_uuid}
)
end
defp get_cases(menu_content) do
menu_content
|> Enum.reduce(
%{},
fn {_str, index}, acc ->
Map.put(
acc,
index,
%{
arguments: [to_string(index)],
type: "has_number_eq",
uuid: Ecto.UUID.generate(),
category_uuid: Ecto.UUID.generate()
}
)
end
)
end
defp add_main_case(content, _node, false), do: content
defp add_main_case(content, %{level: level} = _node, _) when level <= 1, do: content
defp add_main_case(content, _node, true) do
content ++ [{"Press 9 to return to Main Menu", 9}]
end
defp add_back_case(content, _node, false), do: content
defp add_back_case(content, %{level: level} = _node, _) when level <= 2, do: content
defp add_back_case(content, _node, true) do
content ++ [{"Press 8 to return to previous menu", 8}]
end
defp get_categories(menu_content, exits, cases) do
categories =
menu_content
|> Enum.reduce(
%{},
fn {_str, index}, acc ->
Map.put(
acc,
index,
%{
uuid: Map.get(cases, index).category_uuid,
name: to_string(index),
exit_uuid: Map.get(exits, index).uuid
}
)
end
)
# Add Other category
default_category_uuid = Ecto.UUID.generate()
categories =
Map.put(
categories,
10,
%{
uuid: default_category_uuid,
name: "Other",
exit_uuid: Map.get(exits, 10).uuid
}
)
{categories, default_category_uuid}
end
# this is a content node
# generate the message and the localization
@spec gen_flow_content(map(), Menu.t()) :: map()
defp gen_flow_content(json_map, node) do
actions =
[
%{
uuid: node.uuids.action,
attachments: [],
quick_replies: [],
text: language_content(node.content["en"], node.menu_content.content, "en"),
type: "send_msg"
}
]
|> add_label(node, json_map.organization_id)
node_json = %{
uuid: node.uuids.node,
exits: [
%{
uuid: node.uuids.exit,
# At some stage for all content nodes, we'll basically go back to main menu
# for any key pressed
destination_uuid: nil
}
],
actions: actions
}
json_map
|> Map.update!(:nodes, fn n -> [node_json | n] end)
|> add_localization(node, :content)
|> add_ui(node, :content)
end
# Get the content for language
@spec language_content(map(), map(), String.t()) :: any()
defp language_content(content, menu_content, language) do
template = Template.get_template(:content, language)
EEx.eval_string(
template,
language: language,
items: content,
menu_item: Map.get(menu_content, language)
)
end
# get the content for a menu and language
@spec menu_content(map(), String.t(), map(), map()) :: any()
defp menu_content(content, language, node, json_map) do
template = Template.get_template(:menu, language)
EEx.eval_string(
template,
language: language,
items: indexed_content(content, node, json_map)
)
end
@spec add_localization(map(), Menu.t(), atom()) :: map()
defp add_localization(json_map, node, type) do
localization =
node.content
|> Enum.reduce(
json_map.localization,
fn {lang, content}, acc ->
if lang == "en" do
acc
else
text =
if type == :menu,
do: menu_content(content, lang, node, json_map),
else: language_content(content, node.menu_content.content, lang)
Map.update(
acc,
lang,
%{
node.uuids.action => %{text: [text]}
},
fn l -> Map.put(l, node.uuids.action, %{text: [text]}) end
)
end
end
)
Map.put(json_map, :localization, localization)
end
end