Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add multiple choice questions to the exercise description #538

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/howto-write-exercises.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ get the files for the second step, and so on and so forth.
- Separating the grader code

[Step 9 : Introspection of students code](tutorials/step-9.md)

[Step 10 : Create a trivial multiplce choice question exercise](tutorials/step-10.md)
50 changes: 50 additions & 0 deletions docs/tutorials/step-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Step 10: Create a trivial multiplce choice question exercise

In this step, we'll create a simple multiple choice question that only requires the student to answer interactively.

Let's start with the descr.md/html file:

```html
<form name="mc1">
<fieldset>
<legend>1. The correct answer is A!</legend>
<input type="checkbox" name="A"> Choose me!<br>
<input type="checkbox" name="B"> Don't choose me!<br>
<input type="checkbox" name="C"> Don't choose me!<br>
<input type="checkbox" name="D"> Don't choose me!<br>
</fieldset>
</form>
```

The way that we chose to present multiple choice questions was with html forms. Each form has four possible options represented with the name `A`, `B`, `C` or `D`.
The student then select one or more options directly in the exercise description.

The student can then complete the rest of the exercise or grade it to check if his choice was the right one. The grader then verifies which choice was clicked and compares it to the solution file in the form of a string.

The `solution.ml`, for this example should have:

```ocaml
let mc1 = "A"
```

Having the solutions in the form of a string the exercise can have multiple right answer. If thats the case the solution must be written alphabetecly.

Finally, the test function is just a normal function that compares two variables.
In this example the `test.ml` should be:

```ocaml
open Test_lib
open Report

let multipleChoice1_test =
set_progress "A corrigir pergunta 1" ;
Section ([ Text "Exercício 1: " ; Code "solution" ],
test_variable_against_solution
[%ty: string]
"mc1")

let () =
set_result @@
ast_sanity_check code_ast @@ fun () ->
[ multipleChoice1_test ]
```
82 changes: 81 additions & 1 deletion src/app/learnocaml_exercise_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,65 @@ let () =
(* ---- toplevel pane ------------------------------------------------- *)
init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ;
(* ---- text pane ----------------------------------------------------- *)
let mcs = Hashtbl.create 3 in
let find_mc str =
let regex = Str.regexp "let mc[^\n\n]+\n\n" in
try
let start_pos = Str.search_forward regex str 0 in
let end_pos = Str.match_end() in
(start_pos, end_pos)
with Not_found -> (-1, -1)
in
let add_answers str mcid start_pos =
let rec aux id =
let c = str.[id] in
match c with
| '\"' -> ()
| _ -> Hashtbl.add mcs mcid c; aux (id+1)
in
aux (start_pos+11)
in
let remove_substring str (start_pos, end_pos) id =
let prefix = String.sub str 0 start_pos in
let suffix = String.sub str end_pos (String.length str - end_pos) in
add_answers str id start_pos;
prefix ^ suffix
in
let rec remove_all_mc solution count =
let mc = find_mc solution in
if fst mc <> -1 then
let count = count + 1 in
let solution = remove_substring solution mc count in
remove_all_mc solution count
else
solution
in

let solution = remove_all_mc solution 0 in

let tick_mc id form =
let rec tick = function
| [] -> ()
| h::t ->
match h with
| 'A' -> form##.A##setAttribute "checked" "checked"; tick t
| 'B' -> form##.B##click (); tick t
| 'C' -> (form##.C)##click (); tick t
| _ -> (form##.D)##click (); tick t
in
let id = id + 1 in
let answers = Hashtbl.find_all mcs id in
tick answers
in
let restore_mcs () =
let document = Js.Unsafe.global##.document in
let text_div = document##getElementById ("learnocaml-exo-tab-text") in
let iframe = text_div##.lastChild in
let doc = iframe##.contentDocument in
let forms = Js.to_array doc##.forms in
Array.iteri (tick_mc) forms;
in

let text_container = find_component "learnocaml-exo-tab-text" in
let text_iframe = Dom_html.createIframe Dom_html.document in
Manip.replaceChildren text_container
Expand Down Expand Up @@ -237,7 +296,27 @@ let () =
show_loading ~id:"learnocaml-exo-loading" [ messages ; abort_message ]
@@ fun () ->
Lwt_js.sleep 1. >>= fun () ->
let solution = Ace.get_contents ace in

let form_results = Buffer.create 5 in
let isChecked checkbox = checkbox##.checked in
let getCheckedValues i f =
let output = Buffer.create 5 in
if isChecked (f##.A) then Buffer.add_string output ("A");
if isChecked (f##.B) then Buffer.add_string output ("B");
if isChecked (f##.C) then Buffer.add_string output ("C");
if isChecked (f##.D) then Buffer.add_string output ("D");
if Buffer.length output <> 0 then
Buffer.add_string form_results ("let mc" ^ string_of_int (i+1) ^ " = \"" ^ (Buffer.contents output) ^ "\"\n\n")
in
let document = Js.Unsafe.global##.document in
let text_div = document##getElementById ("learnocaml-exo-tab-text") in
let iframe = text_div##.lastChild in
let doc = iframe##.contentDocument in
let forms = Js.to_array (doc##.forms) in
Array.iteri (getCheckedValues) forms;

let solution = (Buffer.contents form_results) ^ (Ace.get_contents ace) in

Learnocaml_toplevel.check top solution >>= fun res ->
match res with
| Toploop_results.Ok ((), _) ->
Expand Down Expand Up @@ -311,5 +390,6 @@ let () =
(* ---- return -------------------------------------------------------- *)
toplevel_launch >>= fun _ ->
typecheck false >>= fun () ->
restore_mcs (); Lwt.return () >>= fun () ->
hide_loading ~id:"learnocaml-exo-loading" () ;
Lwt.return ()