From 13164e68889fa686d34be1a55f9feab069f231db Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:47:52 -0300 Subject: [PATCH 1/7] graded cell example --- .gitignore | 1 + .../lib/grading_server/answer_store.ex | 7 +- grading_server/lib/grading_server/answers.ex | 2 +- grading_server/priv/answers.yml | 40 +---------- modules/grading_client/config/config.exs | 23 ++++++ .../lib/grading_client/application.ex | 12 ++++ .../lib/grading_client/grading_cell.ex | 72 +++++++++++++++++++ modules/grading_client/mix.exs | 5 +- modules/grading_client/mix.lock | 26 +++++++ 9 files changed, 142 insertions(+), 46 deletions(-) create mode 100644 .gitignore create mode 100644 modules/grading_client/config/config.exs create mode 100644 modules/grading_client/lib/grading_client/application.ex create mode 100644 modules/grading_client/lib/grading_client/grading_cell.ex diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/grading_server/lib/grading_server/answer_store.ex b/grading_server/lib/grading_server/answer_store.ex index 2551463..7d36802 100644 --- a/grading_server/lib/grading_server/answer_store.ex +++ b/grading_server/lib/grading_server/answer_store.ex @@ -24,7 +24,7 @@ defmodule GradingServer.AnswerStore do {:ok, state} end - defp key(module_id, question_id), do: "module_#{module_id}-#{question_id}}" + defp key(module_id, question_id), do: "#{module_id}-#{question_id}" @impl true def handle_call({:fetch, module_id, question_id}, _from, _) do @@ -63,12 +63,9 @@ defmodule GradingServer.AnswerStore do end defp map_module({module_name, answers}) do - [_, module_id] = String.split(module_name, "module_") - module_id = String.to_integer(module_id) - Enum.map(answers, fn data -> %Answer{ - module_id: module_id, + module_id: module_name, answer: data["answer"], help_text: data["help_text"], question_id: data["question_id"] diff --git a/grading_server/lib/grading_server/answers.ex b/grading_server/lib/grading_server/answers.ex index e041da3..76e8909 100644 --- a/grading_server/lib/grading_server/answers.ex +++ b/grading_server/lib/grading_server/answers.ex @@ -16,7 +16,7 @@ defmodule GradingServer.Answers do {:incorrect, "Question not found"} %{answer: correct_answer, help_text: help_text} -> - if String.trim(answer) == String.trim(correct_answer) do + if String.trim(inspect(answer)) == String.trim(correct_answer) do :correct else {:incorrect, help_text} diff --git a/grading_server/priv/answers.yml b/grading_server/priv/answers.yml index 9817b88..42ba497 100644 --- a/grading_server/priv/answers.yml +++ b/grading_server/priv/answers.yml @@ -3,42 +3,4 @@ module_1: - question_id: 1 help_text: "This is helpful text that can be used." - answer: "Not a real answer, but an example." - -module_2: - - question_id: 1 - help_text: "" - answer: "" - - question_id: 2 - help_text: "" - answer: "" - -module_3: - - question_id: 1 - help_text: "" - answer: "" - -module_5: - - question_id: 1 - help_text: "" - answer: "" - -module_6: - - question_id: 1 - help_text: "" - answer: "" - -module_7: - - question_id: 1 - help_text: "" - answer: "" - -module_8: - - question_id: 1 - help_text: "" - answer: "" - -module_9: - - question_id: 1 - help_text: "" - answer: "" + answer: ":f" diff --git a/modules/grading_client/config/config.exs b/modules/grading_client/config/config.exs new file mode 100644 index 0000000..d9b0ad7 --- /dev/null +++ b/modules/grading_client/config/config.exs @@ -0,0 +1,23 @@ +import Config + +config :grading_server, answer_store_file: "answers.yml" + +# Configures the endpoint +config :grading_server, GradingServerWeb.Endpoint, + server: false, + url: [host: "localhost"], + render_errors: [view: GradingServerWeb.ErrorView, accepts: ~w(json), layout: false], + pubsub_server: GradingServer.PubSub, + live_view: [signing_salt: "HVVkYc2t"] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +config :simple_token_authentication, + # CHANGE ME IF IN USE + token: "my-token" diff --git a/modules/grading_client/lib/grading_client/application.ex b/modules/grading_client/lib/grading_client/application.ex new file mode 100644 index 0000000..cb91405 --- /dev/null +++ b/modules/grading_client/lib/grading_client/application.ex @@ -0,0 +1,12 @@ +defmodule GradingClient.Application do + use Application + + def start(_type, _args) do + Kino.SmartCell.register(GradingClient.GradingCell) + + children = [] + + opts = [strategy: :one_for_one, name: GradingClient.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/modules/grading_client/lib/grading_client/grading_cell.ex b/modules/grading_client/lib/grading_client/grading_cell.ex new file mode 100644 index 0000000..93186b4 --- /dev/null +++ b/modules/grading_client/lib/grading_client/grading_cell.ex @@ -0,0 +1,72 @@ +defmodule GradingClient.GradingCell do + use Kino.JS + use Kino.JS.Live + use Kino.SmartCell, name: "Graded" + + @impl true + def init(attrs, ctx) do + source = attrs["source"] || "" + + {:ok, assign(ctx, source: source), editor: [source: source, language: "elixir"]} + end + + @impl true + def handle_connect(ctx) do + {:ok, %{}, ctx} + end + + @impl true + def handle_editor_change(source, ctx) do + {:ok, assign(ctx, source: source)} + end + + @impl true + def to_attrs(ctx) do + %{"source" => ctx.assigns.source} + end + + @impl true + def to_source(attrs) do + try do + source = Code.string_to_quoted!(attrs["source"]) + + ast = + quote do + result = unquote(source) + + GradingServer.Answers.check(module_id, question_id, result) + end + + Kino.SmartCell.quoted_to_string(ast) + rescue + error -> + IO.inspect(error) + attrs["source"] + end + end + + asset "main.js" do + """ + export function init(ctx, payload) { + ctx.importCSS("main.css"); + + root.innerHTML = ` +
+ Graded Cell +
+ `; + } + """ + end + + asset "main.css" do + """ + .app { + padding: 8px 16px; + border: solid 1px #cad5e0; + border-radius: 0.5rem 0.5rem 0 0; + border-bottom: none; + } + """ + end +end diff --git a/modules/grading_client/mix.exs b/modules/grading_client/mix.exs index 88f6bb5..7c520e9 100644 --- a/modules/grading_client/mix.exs +++ b/modules/grading_client/mix.exs @@ -14,6 +14,7 @@ defmodule GradingClient.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ + mod: {GradingClient.Application, []}, extra_applications: [:logger] ] end @@ -21,8 +22,10 @@ defmodule GradingClient.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + {:grading_server, path: "#{__DIR__}/../../grading_server"}, {:httpoison, "~> 2.1"}, - {:jason, "~> 1.4"} + {:jason, "~> 1.4"}, + {:kino, "~> 0.10"} ] end end diff --git a/modules/grading_client/mix.lock b/modules/grading_client/mix.lock index c8939aa..a2575ba 100644 --- a/modules/grading_client/mix.lock +++ b/modules/grading_client/mix.lock @@ -1,15 +1,41 @@ %{ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "kino": {:hex, :kino, "0.15.3", "c99e21fc3e5d89513120295b91efc3efd18f7c1fb83875edced9d06ada13a2c0", [:mix], [{:fss, "~> 0.1.0", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "11f62457ce6ac97ad377db9fcde168361fcf0de7db2a47b6f570607dc7897753"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, + "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.9", "4dc5e535832733df68df22f9de168b11c0c74bca65b27b088a10ac36dfb75d04", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1dccb04ec8544340e01608e108f32724458d0ac4b07e551406b3b920c40ba2e5"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, + "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "simple_token_authentication": {:hex, :simple_token_authentication, "0.7.0", "5621af0367ee37d71b90d0d9346fdde097ac1daab18c070252e59db620de2e3e", [:mix], [{:plug, ">= 1.3.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6100de9482b04603b22ecba0b4f93beab1827fdf9dfda6f0dab59830f6815357"}, + "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"}, } From a5caf26b493d02b7ca80a5c67d25ee362f62ae48 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Fri, 4 Apr 2025 21:20:14 -0300 Subject: [PATCH 2/7] feat: self-sufficient grading client --- modules/grading_client/config/config.exs | 23 -------- modules/grading_client/lib/grading_client.ex | 25 +-------- .../lib/grading_client/answer.ex | 11 ++++ .../lib/grading_client/answers.ex | 56 +++++++++++++++++++ .../lib/grading_client/application.ex | 9 ++- .../lib/grading_client/grading_cell.ex | 4 +- modules/grading_client/mix.exs | 3 - modules/grading_client/priv/answers.exs | 24 ++++++++ 8 files changed, 102 insertions(+), 53 deletions(-) delete mode 100644 modules/grading_client/config/config.exs create mode 100644 modules/grading_client/lib/grading_client/answer.ex create mode 100644 modules/grading_client/lib/grading_client/answers.ex create mode 100644 modules/grading_client/priv/answers.exs diff --git a/modules/grading_client/config/config.exs b/modules/grading_client/config/config.exs deleted file mode 100644 index d9b0ad7..0000000 --- a/modules/grading_client/config/config.exs +++ /dev/null @@ -1,23 +0,0 @@ -import Config - -config :grading_server, answer_store_file: "answers.yml" - -# Configures the endpoint -config :grading_server, GradingServerWeb.Endpoint, - server: false, - url: [host: "localhost"], - render_errors: [view: GradingServerWeb.ErrorView, accepts: ~w(json), layout: false], - pubsub_server: GradingServer.PubSub, - live_view: [signing_salt: "HVVkYc2t"] - -# Configures Elixir's Logger -config :logger, :console, - format: "$time $metadata[$level] $message\n", - metadata: [:request_id] - -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason - -config :simple_token_authentication, - # CHANGE ME IF IN USE - token: "my-token" diff --git a/modules/grading_client/lib/grading_client.ex b/modules/grading_client/lib/grading_client.ex index c3de88d..dc05b59 100644 --- a/modules/grading_client/lib/grading_client.ex +++ b/modules/grading_client/lib/grading_client.ex @@ -7,29 +7,8 @@ defmodule GradingClient do Checks the answer to a question. """ - @spec check_answer(any() | String.t(), integer(), integer()) :: :ok | {:error, String.t()} - def check_answer(answer, module_id, question_id) when not is_binary(answer) do - check_answer(module_id, question_id, "#{inspect(answer)}") - end - + @spec check_answer(any(), any(), any()) :: :correct | {:incorrect, String.t() | nil} def check_answer(answer, module_id, question_id) do - # TODO: Make this configurable - url = "http://localhost:4000/api/answers/check" - - headers = [ - {"Content-Type", "application/json"} - ] - - json = Jason.encode!(%{module_id: module_id, question_id: question_id, answer: answer}) - - %{body: body, status_code: 200} = HTTPoison.post!(url, json, headers) - - %{"correct" => is_correct, "help_text" => help_text} = Jason.decode!(body) - - if is_correct do - :ok - else - {:error, help_text} - end + GradingClient.Answers.check(module_id, question_id, answer) end end diff --git a/modules/grading_client/lib/grading_client/answer.ex b/modules/grading_client/lib/grading_client/answer.ex new file mode 100644 index 0000000..57723cf --- /dev/null +++ b/modules/grading_client/lib/grading_client/answer.ex @@ -0,0 +1,11 @@ +defmodule GradingClient.Answer do + @enforce_keys [:question_id, :module_id, :answer, :help_text] + defstruct [:question_id, :module_id, :answer, :help_text] + + @type t :: %__MODULE__{ + module_id: term(), + question_id: term(), + answer: term(), + help_text: term() + } +end diff --git a/modules/grading_client/lib/grading_client/answers.ex b/modules/grading_client/lib/grading_client/answers.ex new file mode 100644 index 0000000..37a3987 --- /dev/null +++ b/modules/grading_client/lib/grading_client/answers.ex @@ -0,0 +1,56 @@ +defmodule GradingClient.Answers do + @moduledoc """ + This module is responsible for checking if an answer is correct. + It uses the `AnswerStore` to fetch the answer and compares if it is correct or not. + """ + use GenServer + + alias GradingClient.Answer + + @table :answer_store + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @impl true + def init(opts) do + :ets.new(@table, [:set, :named_table]) + + filename = opts[:filename] + + {answers, _} = Code.eval_file(filename) + + Enum.each(answers, fn answer -> + :ets.insert(@table, {{answer.module_id, answer.question_id}, answer}) + end) + + {:ok, nil} + end + + @doc """ + Checks if the given answer is correct. + """ + @spec check(integer(), integer(), String.t()) :: :correct | {:incorrect, String.t()} + def check(module_id, question_id, answer) do + GenServer.call(__MODULE__, {:check, module_id, question_id, answer}) + end + + @impl true + def handle_call({:check, module_id, question_id, answer}, _from, state) do + result = + case :ets.lookup(@table, {module_id, question_id}) do + [] -> + {:incorrect, "Question not found"} + + [{_id, %Answer{answer: correct_answer, help_text: help_text}}] -> + if answer == correct_answer do + :correct + else + {:incorrect, help_text} + end + end + + {:reply, result, state} + end +end diff --git a/modules/grading_client/lib/grading_client/application.ex b/modules/grading_client/lib/grading_client/application.ex index cb91405..c7567ba 100644 --- a/modules/grading_client/lib/grading_client/application.ex +++ b/modules/grading_client/lib/grading_client/application.ex @@ -2,9 +2,14 @@ defmodule GradingClient.Application do use Application def start(_type, _args) do - Kino.SmartCell.register(GradingClient.GradingCell) + Kino.SmartCell.register(GradingClient.GradedCell) - children = [] + default_filename = Path.join(:code.priv_dir(:grading_client), "answers.exs") + + children = [ + {GradingClient.Answers, + filename: Application.get_env(:grading_client, :answers_file, default_filename)} + ] opts = [strategy: :one_for_one, name: GradingClient.Supervisor] Supervisor.start_link(children, opts) diff --git a/modules/grading_client/lib/grading_client/grading_cell.ex b/modules/grading_client/lib/grading_client/grading_cell.ex index 93186b4..1685fc5 100644 --- a/modules/grading_client/lib/grading_client/grading_cell.ex +++ b/modules/grading_client/lib/grading_client/grading_cell.ex @@ -1,7 +1,7 @@ -defmodule GradingClient.GradingCell do +defmodule GradingClient.GradedCell do use Kino.JS use Kino.JS.Live - use Kino.SmartCell, name: "Graded" + use Kino.SmartCell, name: "Graded Cell" @impl true def init(attrs, ctx) do diff --git a/modules/grading_client/mix.exs b/modules/grading_client/mix.exs index 7c520e9..9154457 100644 --- a/modules/grading_client/mix.exs +++ b/modules/grading_client/mix.exs @@ -22,9 +22,6 @@ defmodule GradingClient.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:grading_server, path: "#{__DIR__}/../../grading_server"}, - {:httpoison, "~> 2.1"}, - {:jason, "~> 1.4"}, {:kino, "~> 0.10"} ] end diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs new file mode 100644 index 0000000..c91cfcd --- /dev/null +++ b/modules/grading_client/priv/answers.exs @@ -0,0 +1,24 @@ +alias GradingClient.Answer + +to_answers = fn module_name, answers -> + Enum.map(answers, fn data -> + %Answer{ + module_id: module_name, + question_id: data.question_id, + answer: data.answer, + help_text: data[:help_text] + } + end) +end + +owasp_questions = [ + %{ + question_id: 1, + answer: "A", + help_text: "A" + } +] + +List.flatten([ + to_answers.(OWASP, owasp_questions) +]) From 19e47196759c881fa9e4791bd81d326682a04662 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Fri, 4 Apr 2025 22:54:36 -0300 Subject: [PATCH 3/7] feat: self-evaluation and updates for owasp module --- modules/2-owasp.livemd | 138 ++++++++++++++---- modules/grading_client/config/config.exs | 1 + modules/grading_client/lib/grading_client.ex | 2 +- .../lib/grading_client/answers.ex | 26 +++- .../lib/grading_client/graded_cell.ex | 116 +++++++++++++++ .../lib/grading_client/grading_cell.ex | 72 --------- modules/grading_client/mix.exs | 4 +- modules/grading_client/mix.lock | 15 +- modules/grading_client/priv/answers.exs | 9 +- .../test/grading_client_test.exs | 4 - 10 files changed, 269 insertions(+), 118 deletions(-) create mode 100644 modules/grading_client/config/config.exs create mode 100644 modules/grading_client/lib/grading_client/graded_cell.ex delete mode 100644 modules/grading_client/lib/grading_client/grading_cell.ex delete mode 100644 modules/grading_client/test/grading_client_test.exs diff --git a/modules/2-owasp.livemd b/modules/2-owasp.livemd index 9accc72..9f4d04b 100644 --- a/modules/2-owasp.livemd +++ b/modules/2-owasp.livemd @@ -1,14 +1,14 @@ + + # ESCT: Part 2 - OWASP ```elixir -Mix.install([ - {:grading_client, path: "#{__DIR__}/grading_client"}, - :bcrypt_elixir, - :httpoison, - {:absinthe, "~> 1.7.0"}, - {:phoenix, "~> 1.0"}, - {:plug, "~> 1.3.2"} -]) +Mix.install( + [ + {:grading_client, path: "#{__DIR__}/grading_client"} + ], + config_path: "#{__DIR__}/grading_client/config/config.exs" +) md5_hash = :crypto.hash(:md5, "users_password") bcrypt_salted_hash = Bcrypt.hash_pwd_salt("users_password") @@ -103,27 +103,56 @@ Notable CWEs included are CWE-259: Use of Hard-coded Password, CWE-327: Broken o _Please uncomment the function call that you believe is correct._ + + ```elixir -defmodule PasswordCompare do - def option_one(password, md5_hash) do - case :crypto.hash(:md5, password) == md5_hash do - true -> :entry_granted_op1 - false -> :entry_denied_op1 +module_id = Kino.Input.select("Module", [{OWASP, "OWASP"}]) +question_id = Kino.Input.number("Question ID") +Kino.render(Kino.Layout.grid([module_id, question_id], columns: 2)) +nil + +module_id = Kino.Input.read(module_id) +question_id = Kino.Input.read(question_id) + +result = + defmodule PasswordCompare do + def option_one(password, md5_hash) do + case :crypto.hash(:md5, password) == md5_hash do + true -> :entry_granted_op1 + false -> :entry_denied_op1 + end end - end - def option_two(password, bcrypt_salted_hash) do - case Bcrypt.verify_pass(password, bcrypt_salted_hash) do - true -> :entry_granted_op2 - false -> :entry_denied_op2 + def option_two(password, bcrypt_salted_hash) do + case Bcrypt.verify_pass(password, bcrypt_salted_hash) do + true -> :entry_granted_op2 + false -> :entry_denied_op2 + end end end + +case GradingClient.check_answer(module_id, question_id, result) do + :correct -> + IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()]) + + {:incorrect, help_text} when is_binary(help_text) -> + IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text]) + + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) end +``` -# DO NOT CHANGE CODE ABOVE THIS LINE ========================= + -# PasswordCompare.option_one("users_password", md5_hash) -# PasswordCompare.option_two("users_password", bcrypt_salted_hash) +``` +Incorrect: Research MD5 Rainbow Tables +``` + + + +``` +:ok ``` @@ -244,19 +273,58 @@ Notable CWE included is CWE-1104: Use of Unmaintained Third-Party Components ### QUIZ -**Which of the outdated components currently installed is vulnerable?** +**Which of the outdated components listed below is vulnerable?** _Please change the atom below to the name of the vulnerable package installed in this Livebook AND update the afflicted package._ -_HINT: Installed dependencies can be found at the very top, it was the very first cell you ran._ +_HINT: Check the changelogs for each dependency._ + + ```elixir -# CHANGE ME -vulnerable_dependency = :vulnerable_dependency +module_id = Kino.Input.select("Module", [{OWASP, "OWASP"}]) +question_id = Kino.Input.number("Question ID") +Kino.render(Kino.Layout.grid([module_id, question_id], columns: 2)) +nil + +module_id = Kino.Input.read(module_id) +question_id = Kino.Input.read(question_id) + +result = + ( + answer = + Kino.Input.select("Answer", + ecto: "Ecto v2.2.2", + nx: "Nx v0.5.0", + plug: "Plug v1.3.2" + ) + + Kino.render(answer) + Kino.Input.read(answer) + ) + +case GradingClient.check_answer(module_id, question_id, result) do + :correct -> + IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()]) + + {:incorrect, help_text} when is_binary(help_text) -> + IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text]) + + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) +end +``` + + -# DO NOT CHANGE CODE BELOW THIS LINE ============================ -Application.spec(vulnerable_dependency)[:vsn] |> List.to_string() |> IO.puts() -IO.puts(vulnerable_dependency) +``` +Incorrect: Check the changelog for the next minor or major release. +``` + + + +``` +:ok ``` @@ -389,4 +457,18 @@ case HTTPoison.get(user_inputted_url) do end ``` + + +``` +This is the IP belonging to your Livebook instance: +179.241.241.114 + +``` + + + +``` +:ok +``` + [**<- Previous Module: Introduction**](./1-introduction.livemd) || [**Next Module: Secure SDLC Concepts ->**](./3-ssdlc.livemd) diff --git a/modules/grading_client/config/config.exs b/modules/grading_client/config/config.exs new file mode 100644 index 0000000..becde76 --- /dev/null +++ b/modules/grading_client/config/config.exs @@ -0,0 +1 @@ +import Config diff --git a/modules/grading_client/lib/grading_client.ex b/modules/grading_client/lib/grading_client.ex index dc05b59..83efe42 100644 --- a/modules/grading_client/lib/grading_client.ex +++ b/modules/grading_client/lib/grading_client.ex @@ -8,7 +8,7 @@ defmodule GradingClient do """ @spec check_answer(any(), any(), any()) :: :correct | {:incorrect, String.t() | nil} - def check_answer(answer, module_id, question_id) do + def check_answer(module_id, question_id, answer) do GradingClient.Answers.check(module_id, question_id, answer) end end diff --git a/modules/grading_client/lib/grading_client/answers.ex b/modules/grading_client/lib/grading_client/answers.ex index 37a3987..d5f80e7 100644 --- a/modules/grading_client/lib/grading_client/answers.ex +++ b/modules/grading_client/lib/grading_client/answers.ex @@ -21,11 +21,13 @@ defmodule GradingClient.Answers do {answers, _} = Code.eval_file(filename) - Enum.each(answers, fn answer -> - :ets.insert(@table, {{answer.module_id, answer.question_id}, answer}) - end) + modules = + MapSet.new(answers, fn answer -> + :ets.insert(@table, {{answer.module_id, answer.question_id}, answer}) + answer.module_id + end) - {:ok, nil} + {:ok, %{modules: modules}} end @doc """ @@ -36,6 +38,19 @@ defmodule GradingClient.Answers do GenServer.call(__MODULE__, {:check, module_id, question_id, answer}) end + @doc """ + Returns the list of modules. + """ + @spec get_modules() :: [atom()] + def get_modules() do + GenServer.call(__MODULE__, :get_modules) + end + + @impl true + def handle_call(:get_modules, _from, state) do + {:reply, state.modules, state} + end + @impl true def handle_call({:check, module_id, question_id, answer}, _from, state) do result = @@ -44,6 +59,9 @@ defmodule GradingClient.Answers do {:incorrect, "Question not found"} [{_id, %Answer{answer: correct_answer, help_text: help_text}}] -> + dbg(answer) + dbg(correct_answer) + if answer == correct_answer do :correct else diff --git a/modules/grading_client/lib/grading_client/graded_cell.ex b/modules/grading_client/lib/grading_client/graded_cell.ex new file mode 100644 index 0000000..67363f1 --- /dev/null +++ b/modules/grading_client/lib/grading_client/graded_cell.ex @@ -0,0 +1,116 @@ +defmodule GradingClient.GradedCell do + use Kino.JS + use Kino.JS.Live + use Kino.SmartCell, name: "Graded Cell" + + @impl true + def init(attrs, ctx) do + source = attrs["source"] || "" + + {:ok, assign(ctx, source: source, module_id: nil, question_id: nil), + editor: [source: source, language: "elixir"]} + end + + @impl true + def handle_connect(ctx) do + {:ok, %{}, ctx} + end + + @impl true + def handle_editor_change(source, ctx) do + {:ok, assign(ctx, source: source)} + end + + @impl true + def to_attrs(ctx) do + %{ + "source" => ctx.assigns.source, + "module_id" => ctx.assigns.module_id, + "question_id" => ctx.assigns.question_id + } + end + + @impl true + def to_source(attrs) do + dbg(attrs) + + options = Enum.map(GradingClient.Answers.get_modules(), &{&1, inspect(&1)}) + + inputs = + quote do + module_id = Kino.Input.select("Module", unquote(options)) + question_id = Kino.Input.number("Question ID") + + Kino.render(Kino.Layout.grid([module_id, question_id], columns: 2)) + nil + end + + source_ast = + try do + source = Code.string_to_quoted!(attrs["source"]) + + quote do + module_id = Kino.Input.read(module_id) + question_id = Kino.Input.read(question_id) + + result = unquote(source) + + case GradingClient.check_answer(module_id, question_id, result) do + :correct -> + IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()]) + + {:incorrect, help_text} when is_binary(help_text) -> + IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text]) + + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) + end + end + rescue + error -> + IO.inspect(error) + {:<<>>, [delimiter: ~s["""]], [attrs["source"] <> "\n"]} + end + + [ + Kino.SmartCell.quoted_to_string(inputs), + Kino.SmartCell.quoted_to_string(source_ast) + ] + end + + @impl true + def handle_connect(ctx) do + {:ok, %{}, ctx} + end + + asset "main.js" do + """ + export function init(ctx, payload) { + ctx.importCSS("main.css"); + + root.innerHTML = ` +
+
Graded Cell
+
+ `; + } + """ + end + + asset "main.css" do + """ + .app { + padding: 8px 16px; + border: solid 1px #cad5e0; + border-radius: 0.5rem 0.5rem 0 0; + border-bottom: none; + } + + .header { + display: grid; + grid-template-columns: 3fr 1fr 1fr; + gap: 8px; + } + """ + end +end diff --git a/modules/grading_client/lib/grading_client/grading_cell.ex b/modules/grading_client/lib/grading_client/grading_cell.ex deleted file mode 100644 index 1685fc5..0000000 --- a/modules/grading_client/lib/grading_client/grading_cell.ex +++ /dev/null @@ -1,72 +0,0 @@ -defmodule GradingClient.GradedCell do - use Kino.JS - use Kino.JS.Live - use Kino.SmartCell, name: "Graded Cell" - - @impl true - def init(attrs, ctx) do - source = attrs["source"] || "" - - {:ok, assign(ctx, source: source), editor: [source: source, language: "elixir"]} - end - - @impl true - def handle_connect(ctx) do - {:ok, %{}, ctx} - end - - @impl true - def handle_editor_change(source, ctx) do - {:ok, assign(ctx, source: source)} - end - - @impl true - def to_attrs(ctx) do - %{"source" => ctx.assigns.source} - end - - @impl true - def to_source(attrs) do - try do - source = Code.string_to_quoted!(attrs["source"]) - - ast = - quote do - result = unquote(source) - - GradingServer.Answers.check(module_id, question_id, result) - end - - Kino.SmartCell.quoted_to_string(ast) - rescue - error -> - IO.inspect(error) - attrs["source"] - end - end - - asset "main.js" do - """ - export function init(ctx, payload) { - ctx.importCSS("main.css"); - - root.innerHTML = ` -
- Graded Cell -
- `; - } - """ - end - - asset "main.css" do - """ - .app { - padding: 8px 16px; - border: solid 1px #cad5e0; - border-radius: 0.5rem 0.5rem 0 0; - border-bottom: none; - } - """ - end -end diff --git a/modules/grading_client/mix.exs b/modules/grading_client/mix.exs index 9154457..b658b86 100644 --- a/modules/grading_client/mix.exs +++ b/modules/grading_client/mix.exs @@ -22,7 +22,9 @@ defmodule GradingClient.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:kino, "~> 0.10"} + {:kino, "~> 0.10"}, + {:bcrypt_elixir, "~> 3.2"}, + {:httpoison, "~> 2.2"} ] end end diff --git a/modules/grading_client/mix.lock b/modules/grading_client/mix.lock index a2575ba..36afecb 100644 --- a/modules/grading_client/mix.lock +++ b/modules/grading_client/mix.lock @@ -1,22 +1,25 @@ %{ + "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, + "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"}, + "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, + "httpoison": {:hex, :httpoison, "2.2.2", "15420e9e5bbb505b931b2f589dc8be0c3b21e2a91a2c6ba882d99bf8f3ad499d", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "de7ac49fe2ffd89219972fdf39b268582f6f7f68d8cd29b4482dacca1ce82324"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "kino": {:hex, :kino, "0.15.3", "c99e21fc3e5d89513120295b91efc3efd18f7c1fb83875edced9d06ada13a2c0", [:mix], [{:fss, "~> 0.1.0", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "11f62457ce6ac97ad377db9fcde168361fcf0de7db2a47b6f570607dc7897753"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"}, @@ -30,7 +33,7 @@ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "simple_token_authentication": {:hex, :simple_token_authentication, "0.7.0", "5621af0367ee37d71b90d0d9346fdde097ac1daab18c070252e59db620de2e3e", [:mix], [{:plug, ">= 1.3.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6100de9482b04603b22ecba0b4f93beab1827fdf9dfda6f0dab59830f6815357"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs index c91cfcd..bcd5eb8 100644 --- a/modules/grading_client/priv/answers.exs +++ b/modules/grading_client/priv/answers.exs @@ -14,8 +14,13 @@ end owasp_questions = [ %{ question_id: 1, - answer: "A", - help_text: "A" + answer: :entry_granted_op2, + help_text: "Research MD5 Rainbow Tables" + }, + %{ + question_id: 2, + answer: :plug, + help_text: "Check the changelog for the next minor or major release of each option." } ] diff --git a/modules/grading_client/test/grading_client_test.exs b/modules/grading_client/test/grading_client_test.exs deleted file mode 100644 index 759392e..0000000 --- a/modules/grading_client/test/grading_client_test.exs +++ /dev/null @@ -1,4 +0,0 @@ -defmodule GradingClientTest do - use ExUnit.Case - doctest GradingClient -end From 23ff44bd9c6318f27b255033834ac0b9bd270a68 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:00:04 -0300 Subject: [PATCH 4/7] chore: revert grading server --- .../lib/grading_server/answer_store.ex | 7 +++- grading_server/lib/grading_server/answers.ex | 2 +- grading_server/priv/answers.yml | 40 ++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/grading_server/lib/grading_server/answer_store.ex b/grading_server/lib/grading_server/answer_store.ex index 7d36802..2551463 100644 --- a/grading_server/lib/grading_server/answer_store.ex +++ b/grading_server/lib/grading_server/answer_store.ex @@ -24,7 +24,7 @@ defmodule GradingServer.AnswerStore do {:ok, state} end - defp key(module_id, question_id), do: "#{module_id}-#{question_id}" + defp key(module_id, question_id), do: "module_#{module_id}-#{question_id}}" @impl true def handle_call({:fetch, module_id, question_id}, _from, _) do @@ -63,9 +63,12 @@ defmodule GradingServer.AnswerStore do end defp map_module({module_name, answers}) do + [_, module_id] = String.split(module_name, "module_") + module_id = String.to_integer(module_id) + Enum.map(answers, fn data -> %Answer{ - module_id: module_name, + module_id: module_id, answer: data["answer"], help_text: data["help_text"], question_id: data["question_id"] diff --git a/grading_server/lib/grading_server/answers.ex b/grading_server/lib/grading_server/answers.ex index 76e8909..e041da3 100644 --- a/grading_server/lib/grading_server/answers.ex +++ b/grading_server/lib/grading_server/answers.ex @@ -16,7 +16,7 @@ defmodule GradingServer.Answers do {:incorrect, "Question not found"} %{answer: correct_answer, help_text: help_text} -> - if String.trim(inspect(answer)) == String.trim(correct_answer) do + if String.trim(answer) == String.trim(correct_answer) do :correct else {:incorrect, help_text} diff --git a/grading_server/priv/answers.yml b/grading_server/priv/answers.yml index 42ba497..9817b88 100644 --- a/grading_server/priv/answers.yml +++ b/grading_server/priv/answers.yml @@ -3,4 +3,42 @@ module_1: - question_id: 1 help_text: "This is helpful text that can be used." - answer: ":f" + answer: "Not a real answer, but an example." + +module_2: + - question_id: 1 + help_text: "" + answer: "" + - question_id: 2 + help_text: "" + answer: "" + +module_3: + - question_id: 1 + help_text: "" + answer: "" + +module_5: + - question_id: 1 + help_text: "" + answer: "" + +module_6: + - question_id: 1 + help_text: "" + answer: "" + +module_7: + - question_id: 1 + help_text: "" + answer: "" + +module_8: + - question_id: 1 + help_text: "" + answer: "" + +module_9: + - question_id: 1 + help_text: "" + answer: "" From 19d56d3097acdaea4b4c4ca948aad8ba0b4d6b66 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:11:05 -0300 Subject: [PATCH 5/7] fix: use comment instead of kino inputs --- modules/2-owasp.livemd | 98 ++++++++----------- .../lib/grading_client/graded_cell.ex | 54 +++++----- 2 files changed, 69 insertions(+), 83 deletions(-) diff --git a/modules/2-owasp.livemd b/modules/2-owasp.livemd index 9f4d04b..02fa5de 100644 --- a/modules/2-owasp.livemd +++ b/modules/2-owasp.livemd @@ -1,5 +1,3 @@ - - # ESCT: Part 2 - OWASP ```elixir @@ -103,17 +101,9 @@ Notable CWEs included are CWE-259: Use of Hard-coded Password, CWE-327: Broken o _Please uncomment the function call that you believe is correct._ - + ```elixir -module_id = Kino.Input.select("Module", [{OWASP, "OWASP"}]) -question_id = Kino.Input.number("Question ID") -Kino.render(Kino.Layout.grid([module_id, question_id], columns: 2)) -nil - -module_id = Kino.Input.read(module_id) -question_id = Kino.Input.read(question_id) - result = defmodule PasswordCompare do def option_one(password, md5_hash) do @@ -131,6 +121,25 @@ result = end end +[module_id, question_id] = + "#OWASP:1\ndefmodule PasswordCompare do\n def option_one(password, md5_hash) do\n case :crypto.hash(:md5, password) == md5_hash do\n true -> :entry_granted_op1\n false -> :entry_denied_op1\n end\n end\n\n def option_two(password, bcrypt_salted_hash) do\n case Bcrypt.verify_pass(password, bcrypt_salted_hash) do\n true -> :entry_granted_op2\n false -> :entry_denied_op2\n end\n end\nend\n\n# DO NOT CHANGE CODE ABOVE THIS LINE =========================\n\n# PasswordCompare.option_one(\"users_password\", md5_hash)\n# PasswordCompare.option_two(\"users_password\", bcrypt_salted_hash)" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{"OWASP" => OWASP}[String.trim(module_id)] do + nil -> raise "invalid module id: #{module_id}" + module_id -> module_id + end + +question_id = + case Integer.parse(String.trim(question_id)) do + {id, ""} -> id + _ -> raise "invalid question id: #{question_id}" + end + case GradingClient.check_answer(module_id, question_id, result) do :correct -> IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()]) @@ -143,18 +152,6 @@ case GradingClient.check_answer(module_id, question_id, result) do end ``` - - -``` -Incorrect: Research MD5 Rainbow Tables -``` - - - -``` -:ok -``` - ## Injection @@ -279,17 +276,9 @@ _Please change the atom below to the name of the vulnerable package installed in _HINT: Check the changelogs for each dependency._ - + ```elixir -module_id = Kino.Input.select("Module", [{OWASP, "OWASP"}]) -question_id = Kino.Input.number("Question ID") -Kino.render(Kino.Layout.grid([module_id, question_id], columns: 2)) -nil - -module_id = Kino.Input.read(module_id) -question_id = Kino.Input.read(question_id) - result = ( answer = @@ -303,6 +292,25 @@ result = Kino.Input.read(answer) ) +[module_id, question_id] = + "#OWASP:2\nanswer = \n Kino.Input.select(\"Answer\", [\n {:ecto, \"Ecto v2.2.2\"},\n {:nx, \"Nx v0.5.0\"},\n {:plug, \"Plug v1.3.2\"}\n ])\n\nKino.render(answer)\n\nKino.Input.read(answer)" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{"OWASP" => OWASP}[String.trim(module_id)] do + nil -> raise "invalid module id: #{module_id}" + module_id -> module_id + end + +question_id = + case Integer.parse(String.trim(question_id)) do + {id, ""} -> id + _ -> raise "invalid question id: #{question_id}" + end + case GradingClient.check_answer(module_id, question_id, result) do :correct -> IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()]) @@ -315,18 +323,6 @@ case GradingClient.check_answer(module_id, question_id, result) do end ``` - - -``` -Incorrect: Check the changelog for the next minor or major release. -``` - - - -``` -:ok -``` - ## Identification and Authentication Failures @@ -457,18 +453,4 @@ case HTTPoison.get(user_inputted_url) do end ``` - - -``` -This is the IP belonging to your Livebook instance: -179.241.241.114 - -``` - - - -``` -:ok -``` - [**<- Previous Module: Introduction**](./1-introduction.livemd) || [**Next Module: Secure SDLC Concepts ->**](./3-ssdlc.livemd) diff --git a/modules/grading_client/lib/grading_client/graded_cell.ex b/modules/grading_client/lib/grading_client/graded_cell.ex index 67363f1..18f1761 100644 --- a/modules/grading_client/lib/grading_client/graded_cell.ex +++ b/modules/grading_client/lib/grading_client/graded_cell.ex @@ -11,11 +11,6 @@ defmodule GradingClient.GradedCell do editor: [source: source, language: "elixir"]} end - @impl true - def handle_connect(ctx) do - {:ok, %{}, ctx} - end - @impl true def handle_editor_change(source, ctx) do {:ok, assign(ctx, source: source)} @@ -32,29 +27,41 @@ defmodule GradingClient.GradedCell do @impl true def to_source(attrs) do - dbg(attrs) - - options = Enum.map(GradingClient.Answers.get_modules(), &{&1, inspect(&1)}) - - inputs = - quote do - module_id = Kino.Input.select("Module", unquote(options)) - question_id = Kino.Input.number("Question ID") - - Kino.render(Kino.Layout.grid([module_id, question_id], columns: 2)) - nil - end + modules = Map.new(GradingClient.Answers.get_modules(), &{inspect(&1), &1}) source_ast = try do - source = Code.string_to_quoted!(attrs["source"]) + source_attr = attrs["source"] + source = Code.string_to_quoted!(source_attr) quote do - module_id = Kino.Input.read(module_id) - question_id = Kino.Input.read(question_id) - result = unquote(source) + [module_id, question_id] = + unquote(source_attr) + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + + module_id = + case unquote(Macro.escape(modules))[String.trim(module_id)] do + nil -> + raise "invalid module id: #{module_id}" + + module_id -> + module_id + end + + question_id = + case Integer.parse(String.trim(question_id)) do + {id, ""} -> + id + + _ -> + raise "invalid question id: #{question_id}" + end + case GradingClient.check_answer(module_id, question_id, result) do :correct -> IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()]) @@ -72,10 +79,7 @@ defmodule GradingClient.GradedCell do {:<<>>, [delimiter: ~s["""]], [attrs["source"] <> "\n"]} end - [ - Kino.SmartCell.quoted_to_string(inputs), - Kino.SmartCell.quoted_to_string(source_ast) - ] + Kino.SmartCell.quoted_to_string(source_ast) end @impl true From 1096c2c264c6a48aba4c3ddacdd2296cec688115 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:19:43 -0300 Subject: [PATCH 6/7] chore: cleanup stray changes --- .../lib/grading_client/graded_cell.ex | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/modules/grading_client/lib/grading_client/graded_cell.ex b/modules/grading_client/lib/grading_client/graded_cell.ex index 18f1761..d4e7355 100644 --- a/modules/grading_client/lib/grading_client/graded_cell.ex +++ b/modules/grading_client/lib/grading_client/graded_cell.ex @@ -7,8 +7,7 @@ defmodule GradingClient.GradedCell do def init(attrs, ctx) do source = attrs["source"] || "" - {:ok, assign(ctx, source: source, module_id: nil, question_id: nil), - editor: [source: source, language: "elixir"]} + {:ok, assign(ctx, source: source), editor: [source: source, language: "elixir"]} end @impl true @@ -18,11 +17,7 @@ defmodule GradingClient.GradedCell do @impl true def to_attrs(ctx) do - %{ - "source" => ctx.assigns.source, - "module_id" => ctx.assigns.module_id, - "question_id" => ctx.assigns.question_id - } + %{"source" => ctx.assigns.source} end @impl true @@ -93,7 +88,7 @@ defmodule GradingClient.GradedCell do ctx.importCSS("main.css"); root.innerHTML = ` -
+
Graded Cell
`; @@ -109,12 +104,6 @@ defmodule GradingClient.GradedCell do border-radius: 0.5rem 0.5rem 0 0; border-bottom: none; } - - .header { - display: grid; - grid-template-columns: 3fr 1fr 1fr; - gap: 8px; - } """ end end From b38f4a3413b36a312e750a32f10033c077d81987 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Sat, 5 Apr 2025 10:11:04 -0300 Subject: [PATCH 7/7] Update modules/grading_client/lib/grading_client/answers.ex Co-authored-by: Dave Lucia --- modules/grading_client/lib/grading_client/answers.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/grading_client/lib/grading_client/answers.ex b/modules/grading_client/lib/grading_client/answers.ex index d5f80e7..af3e9d2 100644 --- a/modules/grading_client/lib/grading_client/answers.ex +++ b/modules/grading_client/lib/grading_client/answers.ex @@ -59,8 +59,6 @@ defmodule GradingClient.Answers do {:incorrect, "Question not found"} [{_id, %Answer{answer: correct_answer, help_text: help_text}}] -> - dbg(answer) - dbg(correct_answer) if answer == correct_answer do :correct