diff --git a/demo-repository/exercises/demo3/demo3-etu1/descr.html b/demo-repository/exercises/demo3/demo3-etu1/descr.html new file mode 100644 index 000000000..174931593 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu1/descr.html @@ -0,0 +1,113 @@ +

+ This exercise is just a demo for the exercise environment. +

+ +

The task

+ +

+ In this test-bed exercise you are asked to implement the basic + integer-arithmetic functions. +

+ + +
    +
  1. + Write a function plus of type int -> int -> int. +
    + Ex : val plus 3 4 : int = 7. +
  2. +
  3. + Write a function minus of type int -> int -> int. +
    + Ex : val minus 3 2 : int = 1. +
  4. +
  5. + Write a function times of type int -> int -> int. +
    + Ex : val times 2 6 : int = 12. +
  6. +
  7. + Write a function divide of type int -> int -> int. + Ex : val divide 8 4 : int = 2. +
  8. +
  9. + Write a function sum3 of type int -> int -> int -> int + . +
    + Ex : val sum3 2 4 3 : int = 9. +
  10. +
  11. + Write a function plus2 of type int -> int using plus + which add 2 to another integer. +
  12. +
+

+ Now you have done these quite easy exercises, sure you can train on recursive + fonctions ! +

+ +

Recursive exercises

+ +

+ In this test-bed exercise you are asked to implement some recursive + integer-arithmetic functions. +

+
    +
  1. + Write a function sum_0_to_n of type int -> int which + add integers from 0 to n, considering n can't be under 0. +
    + Ex : val sum_0_to_n 3 : int = 6. +
  2. +
  3. + Write a function fact of type int -> int which represent + the factorial (n!) function : 4! = 1x2x3x4 = 24. +
    + Ex : val fact 5 : int = 120. +
  4. +
+ +

List exercise

+ +
    + + Write a function list_0_to_n of type int -> int list + . + This function must create a list containing integers between 0 and n. If + n<0, the function must return a Failure exception. +
    + Ex :list_0_to_n 4 -> [0;1;2;3;4] +
    + + Write a function forall of type 'a list -> + ('a -> bool) -> bool , that return true if (all) the elements of + the list verify a predicat. +
    + Ex : forall [0;2;4] (fun x -> (x%2)=0) -> true +
    + + Write a function cat_list of type 'a list -> 'a list -> + 'a list . This function concatenate two list to make only one. +
    + Ex : cat_list ["a";"b"] ["c";"d";"e"] -> ["a";"b";"c";"d";"e"] +
    +
+ + + + + +

+
+ Feel free to introduce more errors and to stress the system, + the resulting grade for this exercise will not be taken into account + in the global grade and you might submit as many solutions as you + wish. +
+ If you end up writing an infinite computation, the system will + detect it after a while and ask you to stop the script. It will + slow your browser down until that point, since everything is done + on your side, via your JavaScript engine. + So don't worry, you can try and break the system as much as you + want, it should not break anything on our servers. +

diff --git a/demo-repository/exercises/demo3/demo3-etu1/max_score.txt b/demo-repository/exercises/demo3/demo3-etu1/max_score.txt new file mode 100644 index 000000000..04f9fe460 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu1/max_score.txt @@ -0,0 +1 @@ +59 diff --git a/demo-repository/exercises/demo3/demo3-etu1/prelude.ml b/demo-repository/exercises/demo3/demo3-etu1/prelude.ml new file mode 100644 index 000000000..8436a905e --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu1/prelude.ml @@ -0,0 +1,2 @@ +(* Some code is loaded in the toplevel before your code. *) +let greetings = "Hello world!" diff --git a/demo-repository/exercises/demo3/demo3-etu1/prepare.ml b/demo-repository/exercises/demo3/demo3-etu1/prepare.ml new file mode 100644 index 000000000..e69de29bb diff --git a/demo-repository/exercises/demo3/demo3-etu1/solution.ml b/demo-repository/exercises/demo3/demo3-etu1/solution.ml new file mode 100644 index 000000000..14392c9e8 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu1/solution.ml @@ -0,0 +1,11 @@ + +(* First part : simple functions on integers *) + +let plus = (+);; + +let minus = (-);; + +let times a b = a*b;; + +let divide = (/);; + diff --git a/demo-repository/exercises/demo3/demo3-etu1/template.ml b/demo-repository/exercises/demo3/demo3-etu1/template.ml new file mode 100644 index 000000000..7d6827247 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu1/template.ml @@ -0,0 +1 @@ +(* Write here your code *) diff --git a/demo-repository/exercises/demo3/demo3-etu1/test.ml b/demo-repository/exercises/demo3/demo3-etu1/test.ml new file mode 100644 index 000000000..8bdd25eba --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu1/test.ml @@ -0,0 +1,54 @@ +open Test_lib +open Report + +module Mutation_test = Mutation_test.Make (Test_lib) +open Mutation_test + +let test_plus () = + test_function_2_against_solution + [%ty : int -> int -> int ] "plus" + [ (1, 1) ; (2, 2) ; (10, -10) ] + (* @ + test_unit_tests_2 + [%ty : int -> int -> int ] "plus" + [ ("Subtracts instead of adding", 1, fun x y -> x - y) ] *) + + (* Au dessus (en commentaire) : test des tests de l'élève *) + +let test_minus () = + test_function_2_against_solution + [%ty : int -> int -> int ] "minus" + [ (1, 1) ; (4, -2) ; (0, 10) ] + + +let test_times () = + test_function_2_against_solution + [%ty : int -> int -> int ] "times" + [ (1, 3) ; (2, 4) ; (3, 0) ] + + +let test_divide () = + test_function_2_against_solution + [%ty : int -> int -> int ] "divide" + [ (12, 4) ; (12, 5) ; (3, 0) ] + + + + +let () = + set_result @@ + ast_sanity_check code_ast @@ fun () -> + [ Section + ([ Text "Function:" ; Code "plus" ], + test_plus ()) ; + Section + ([ Text "Function:" ; Code "minus" ], + test_minus ()) ; + Section + ([ Text "Function:" ; Code "times" ], + test_times ()) ; + Section + ([ Text "Function:" ; Code "divide" ], + test_divide ()) ; + + ] diff --git a/demo-repository/exercises/demo3/demo3-etu2/descr.html b/demo-repository/exercises/demo3/demo3-etu2/descr.html new file mode 100644 index 000000000..174931593 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu2/descr.html @@ -0,0 +1,113 @@ +

+ This exercise is just a demo for the exercise environment. +

+ +

The task

+ +

+ In this test-bed exercise you are asked to implement the basic + integer-arithmetic functions. +

+ + +
    +
  1. + Write a function plus of type int -> int -> int. +
    + Ex : val plus 3 4 : int = 7. +
  2. +
  3. + Write a function minus of type int -> int -> int. +
    + Ex : val minus 3 2 : int = 1. +
  4. +
  5. + Write a function times of type int -> int -> int. +
    + Ex : val times 2 6 : int = 12. +
  6. +
  7. + Write a function divide of type int -> int -> int. + Ex : val divide 8 4 : int = 2. +
  8. +
  9. + Write a function sum3 of type int -> int -> int -> int + . +
    + Ex : val sum3 2 4 3 : int = 9. +
  10. +
  11. + Write a function plus2 of type int -> int using plus + which add 2 to another integer. +
  12. +
+

+ Now you have done these quite easy exercises, sure you can train on recursive + fonctions ! +

+ +

Recursive exercises

+ +

+ In this test-bed exercise you are asked to implement some recursive + integer-arithmetic functions. +

+
    +
  1. + Write a function sum_0_to_n of type int -> int which + add integers from 0 to n, considering n can't be under 0. +
    + Ex : val sum_0_to_n 3 : int = 6. +
  2. +
  3. + Write a function fact of type int -> int which represent + the factorial (n!) function : 4! = 1x2x3x4 = 24. +
    + Ex : val fact 5 : int = 120. +
  4. +
+ +

List exercise

+ +
    + + Write a function list_0_to_n of type int -> int list + . + This function must create a list containing integers between 0 and n. If + n<0, the function must return a Failure exception. +
    + Ex :list_0_to_n 4 -> [0;1;2;3;4] +
    + + Write a function forall of type 'a list -> + ('a -> bool) -> bool , that return true if (all) the elements of + the list verify a predicat. +
    + Ex : forall [0;2;4] (fun x -> (x%2)=0) -> true +
    + + Write a function cat_list of type 'a list -> 'a list -> + 'a list . This function concatenate two list to make only one. +
    + Ex : cat_list ["a";"b"] ["c";"d";"e"] -> ["a";"b";"c";"d";"e"] +
    +
+ + + + + +

+
+ Feel free to introduce more errors and to stress the system, + the resulting grade for this exercise will not be taken into account + in the global grade and you might submit as many solutions as you + wish. +
+ If you end up writing an infinite computation, the system will + detect it after a while and ask you to stop the script. It will + slow your browser down until that point, since everything is done + on your side, via your JavaScript engine. + So don't worry, you can try and break the system as much as you + want, it should not break anything on our servers. +

diff --git a/demo-repository/exercises/demo3/demo3-etu2/max_score.txt b/demo-repository/exercises/demo3/demo3-etu2/max_score.txt new file mode 100644 index 000000000..04f9fe460 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu2/max_score.txt @@ -0,0 +1 @@ +59 diff --git a/demo-repository/exercises/demo3/demo3-etu2/prelude.ml b/demo-repository/exercises/demo3/demo3-etu2/prelude.ml new file mode 100644 index 000000000..92345ec5c --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu2/prelude.ml @@ -0,0 +1,2 @@ +(* Some code is loaded in the toplevel before your code. *) +let greetings2 = "Hello world! This is second part of demo3" diff --git a/demo-repository/exercises/demo3/demo3-etu2/prepare.ml b/demo-repository/exercises/demo3/demo3-etu2/prepare.ml new file mode 100644 index 000000000..e69de29bb diff --git a/demo-repository/exercises/demo3/demo3-etu2/solution.ml b/demo-repository/exercises/demo3/demo3-etu2/solution.ml new file mode 100644 index 000000000..8c56db937 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu2/solution.ml @@ -0,0 +1,20 @@ + +(* First part : simple functions on integers *) + +let sum3 a b c = a+b+c;; + +let plus2 a = a+2 ;; + +(* Second part : recursive functions on integers *) + +let rec sum_0_to_n n = + if n<0 then failwith "sum_0_to_n : arg < 0 not allowed" else + match n with + |0-> 0 + |n -> n+sum_0_to_n (n-1);; + +let rec fact n = + match n with + | 0 | 1 -> 1 + | n -> n*fact (n-1);; + diff --git a/demo-repository/exercises/demo3/demo3-etu2/template.ml b/demo-repository/exercises/demo3/demo3-etu2/template.ml new file mode 100644 index 000000000..7d6827247 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu2/template.ml @@ -0,0 +1 @@ +(* Write here your code *) diff --git a/demo-repository/exercises/demo3/demo3-etu2/test.ml b/demo-repository/exercises/demo3/demo3-etu2/test.ml new file mode 100644 index 000000000..7b9e93f5f --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-etu2/test.ml @@ -0,0 +1,63 @@ +open Test_lib +open Report + +module Mutation_test = Mutation_test.Make (Test_lib) +open Mutation_test + +let test_sum3 () = + test_function_against_solution + ~gen:2 (* only 2 random test cases *) + [%funty: int -> int -> int -> int] (* function type *) + "sum3" (* function name *) + (* list of additional, explicit test cases *) + [ 10 @: 20 @:!! 30 ; + -1 @: -2 @:!! -3 ] + +let test_plus2 () = + test_function_1_against_solution + [%ty: int -> int] (* function type *) + "plus2" + ~gen:2 + [ 12;6 ] + + +let test_sum_0_to_n () = + test_function_1_against_solution + [%ty : int -> int ] + "sum_0_to_n" + ~gen:4 + ~test:(test_eq_exn + (fun exn1 exn2 -> match exn1, exn2 with + Failure _, Failure _ -> true | _, _ -> false)) + [-4] + +let test_fact () = + Section ([ Text "Function" ; Code "fact"], + test_function_1_against_solution + [%ty : int -> int ] + "fact" + ~sampler:(fun () -> Random.int 11) + ~gen:4 + [] + ) + + + + + +let () = + set_result @@ + ast_sanity_check code_ast @@ fun () -> + [ Section + ([ Text "Function:" ; Code "sum3" ], + test_sum3 ()) ; + Section + ([ Text "Function:" ; Code "plus2"], + test_plus2 ()); + Section + ([ Text "Function:" ; Code "sum_0_to_n"], + test_sum_0_to_n ()); + + test_fact (); + + ] diff --git a/demo-repository/exercises/demo3/demo3-prof/descr.html b/demo-repository/exercises/demo3/demo3-prof/descr.html new file mode 100644 index 000000000..174931593 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-prof/descr.html @@ -0,0 +1,113 @@ +

+ This exercise is just a demo for the exercise environment. +

+ +

The task

+ +

+ In this test-bed exercise you are asked to implement the basic + integer-arithmetic functions. +

+ + +
    +
  1. + Write a function plus of type int -> int -> int. +
    + Ex : val plus 3 4 : int = 7. +
  2. +
  3. + Write a function minus of type int -> int -> int. +
    + Ex : val minus 3 2 : int = 1. +
  4. +
  5. + Write a function times of type int -> int -> int. +
    + Ex : val times 2 6 : int = 12. +
  6. +
  7. + Write a function divide of type int -> int -> int. + Ex : val divide 8 4 : int = 2. +
  8. +
  9. + Write a function sum3 of type int -> int -> int -> int + . +
    + Ex : val sum3 2 4 3 : int = 9. +
  10. +
  11. + Write a function plus2 of type int -> int using plus + which add 2 to another integer. +
  12. +
+

+ Now you have done these quite easy exercises, sure you can train on recursive + fonctions ! +

+ +

Recursive exercises

+ +

+ In this test-bed exercise you are asked to implement some recursive + integer-arithmetic functions. +

+
    +
  1. + Write a function sum_0_to_n of type int -> int which + add integers from 0 to n, considering n can't be under 0. +
    + Ex : val sum_0_to_n 3 : int = 6. +
  2. +
  3. + Write a function fact of type int -> int which represent + the factorial (n!) function : 4! = 1x2x3x4 = 24. +
    + Ex : val fact 5 : int = 120. +
  4. +
+ +

List exercise

+ +
    + + Write a function list_0_to_n of type int -> int list + . + This function must create a list containing integers between 0 and n. If + n<0, the function must return a Failure exception. +
    + Ex :list_0_to_n 4 -> [0;1;2;3;4] +
    + + Write a function forall of type 'a list -> + ('a -> bool) -> bool , that return true if (all) the elements of + the list verify a predicat. +
    + Ex : forall [0;2;4] (fun x -> (x%2)=0) -> true +
    + + Write a function cat_list of type 'a list -> 'a list -> + 'a list . This function concatenate two list to make only one. +
    + Ex : cat_list ["a";"b"] ["c";"d";"e"] -> ["a";"b";"c";"d";"e"] +
    +
+ + + + + +

+
+ Feel free to introduce more errors and to stress the system, + the resulting grade for this exercise will not be taken into account + in the global grade and you might submit as many solutions as you + wish. +
+ If you end up writing an infinite computation, the system will + detect it after a while and ask you to stop the script. It will + slow your browser down until that point, since everything is done + on your side, via your JavaScript engine. + So don't worry, you can try and break the system as much as you + want, it should not break anything on our servers. +

diff --git a/demo-repository/exercises/demo3/demo3-prof/max_score.txt b/demo-repository/exercises/demo3/demo3-prof/max_score.txt new file mode 100644 index 000000000..04f9fe460 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-prof/max_score.txt @@ -0,0 +1 @@ +59 diff --git a/demo-repository/exercises/demo3/demo3-prof/prelude.ml b/demo-repository/exercises/demo3/demo3-prof/prelude.ml new file mode 100644 index 000000000..8436a905e --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-prof/prelude.ml @@ -0,0 +1,2 @@ +(* Some code is loaded in the toplevel before your code. *) +let greetings = "Hello world!" diff --git a/demo-repository/exercises/demo3/demo3-prof/prepare.ml b/demo-repository/exercises/demo3/demo3-prof/prepare.ml new file mode 100644 index 000000000..e69de29bb diff --git a/demo-repository/exercises/demo3/demo3-prof/solution.ml b/demo-repository/exercises/demo3/demo3-prof/solution.ml new file mode 100644 index 000000000..f07be0fac --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-prof/solution.ml @@ -0,0 +1,28 @@ + +(* First part : simple functions on integers *) + +let plus = (+);; + +let minus = (-);; + +let times a b = a*b;; + +let divide = (/);; + +let sum3 a b c = a+b+c;; + +let plus2 a = plus 2 a ;; + +(* Second part : recursive functions on integers *) + +let rec sum_0_to_n n = + if n<0 then failwith "sum_0_to_n : arg < 0 not allowed" else + match n with + |0-> 0 + |n -> n+sum_0_to_n (n-1);; + +let rec fact n = + match n with + | 0 | 1 -> 1 + | n -> n*fact (n-1);; + diff --git a/demo-repository/exercises/demo3/demo3-prof/template.ml b/demo-repository/exercises/demo3/demo3-prof/template.ml new file mode 100644 index 000000000..7d6827247 --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-prof/template.ml @@ -0,0 +1 @@ +(* Write here your code *) diff --git a/demo-repository/exercises/demo3/demo3-prof/test.ml b/demo-repository/exercises/demo3/demo3-prof/test.ml new file mode 100644 index 000000000..5a2a81d2a --- /dev/null +++ b/demo-repository/exercises/demo3/demo3-prof/test.ml @@ -0,0 +1,105 @@ +open Test_lib +open Report + +module Mutation_test = Mutation_test.Make (Test_lib) +open Mutation_test + +let test_plus () = + test_function_2_against_solution + [%ty : int -> int -> int ] "plus" + [ (1, 1) ; (2, 2) ; (10, -10) ] + (* @ + test_unit_tests_2 + [%ty : int -> int -> int ] "plus" + [ ("Subtracts instead of adding", 1, fun x y -> x - y) ] *) + + (* Au dessus (en commentaire) : test des tests de l'élève *) + +let test_minus () = + test_function_2_against_solution + [%ty : int -> int -> int ] "minus" + [ (1, 1) ; (4, -2) ; (0, 10) ] + + +let test_times () = + test_function_2_against_solution + [%ty : int -> int -> int ] "times" + [ (1, 3) ; (2, 4) ; (3, 0) ] + + +let test_divide () = + test_function_2_against_solution + [%ty : int -> int -> int ] "divide" + [ (12, 4) ; (12, 5) ; (3, 0) ] + + + +let test_sum3 () = + test_function_against_solution + ~gen:2 (* only 2 random test cases *) + [%funty: int -> int -> int -> int] (* function type *) + "sum3" (* function name *) + (* list of additional, explicit test cases *) + [ 10 @: 20 @:!! 30 ; + -1 @: -2 @:!! -3 ] + +let test_plus2 () = + test_function_1_against_solution + [%ty: int -> int] (* function type *) + "plus2" + ~gen:2 + [ 12;6 ] + + +let test_sum_0_to_n () = + test_function_1_against_solution + [%ty : int -> int ] + "sum_0_to_n" + ~gen:4 + ~test:(test_eq_exn + (fun exn1 exn2 -> match exn1, exn2 with + Failure _, Failure _ -> true | _, _ -> false)) + [-4] + +let test_fact () = + Section ([ Text "Function" ; Code "fact"], + test_function_1_against_solution + [%ty : int -> int ] + "fact" + ~sampler:(fun () -> Random.int 11) + ~gen:4 + [] + ) + + + + + +let () = + set_result @@ + ast_sanity_check code_ast @@ fun () -> + [ Section + ([ Text "Function:" ; Code "plus" ], + test_plus ()) ; + Section + ([ Text "Function:" ; Code "minus" ], + test_minus ()) ; + Section + ([ Text "Function:" ; Code "times" ], + test_times ()) ; + Section + ([ Text "Function:" ; Code "divide" ], + test_divide ()) ; + Section + ([ Text "Function:" ; Code "sum3" ], + test_sum3 ()) ; + Section + ([ Text "Function:" ; Code "plus2"], + test_plus2 ()); + Section + ([ Text "Function:" ; Code "sum_0_to_n"], + test_sum_0_to_n ()); + + test_fact (); + + ] diff --git a/demo-repository/exercises/demo3/subindex.json b/demo-repository/exercises/demo3/subindex.json new file mode 100644 index 000000000..0199c4119 --- /dev/null +++ b/demo-repository/exercises/demo3/subindex.json @@ -0,0 +1,32 @@ +{ "learnocaml_version": "1", + "meta":{ + "learnocaml_version": "2", + "kind": "problem", + "stars": 4, + "title": "Demo of a multi-part exercise", + "identifier": "demo3", + "authors": [["Someone", "someone@example.com"]], + "focus": ["recursion"], + "requirements": ["basic arithmetic"], + "forward_exercises": [], + "backward_exercises": []} , + "check_all_against": "demo3-prof", + "parts": [ + { "subtitle": "First test", + "subexercise": "demo3-etu1", + "student_weight": 1, + "teacher_weight": 0 + }, + { "subtitle": "Second test", + "subexercise": "demo3-etu2", + "student_hidden": false, + "student_weight": 1, + "teacher_weight": 0 + }, + { "subtitle": "Grading", + "subexercise": "demo3-prof", + "student_hidden": true, + "student_weight": 0, + "teacher_weight": 1 + }] +} diff --git a/src/app/learnocaml_common.ml b/src/app/learnocaml_common.ml index bd9c10aff..6226695d5 100644 --- a/src/app/learnocaml_common.ml +++ b/src/app/learnocaml_common.ml @@ -569,10 +569,10 @@ let exercise_text ex_meta exo = let descr = let lang = "" in try - List.assoc lang (Learnocaml_exercise.(access File.descr exo)) + List.assoc lang (Learnocaml_exercise.(access false File.descr exo)) with Not_found -> - try List.assoc "" (Learnocaml_exercise.(access File.descr exo)) + try List.assoc "" (Learnocaml_exercise.(access false File.descr exo)) with Not_found -> [%i "No description available for this exercise." ] in Format.asprintf @@ -941,6 +941,42 @@ let set_nickname_div () = | nickname -> Manip.setInnerText nickname_div nickname | exception Not_found -> () +(* setup for the prelude in the description page (in description_main.ml) *) +let setup_tab_text_prelude_pane prelude = + if prelude = "" then () else + let iframe_pane = find_component "learnocaml-exo-tab-text-iframe" in + let prelude_pane = find_component "learnocaml-exo-tab-text-prelude" in + let open Tyxml_js.Html5 in + let state = + ref (match arg "tab_text_prelude" with + | exception Not_found -> false + | "shown" -> true + | "hidden" -> false + | _ -> failwith "Bad format for argument prelude.") in + let prelude_btn = button [] in + let prelude_title = h1 [ txt [%i"OCaml prelude"] ; + prelude_btn ] in + let prelude_container = + pre ~a: [ a_class [ "toplevel-code" ] ] + (Learnocaml_toplevel_output.format_ocaml_code prelude) in + let update () = + if !state then begin + Manip.replaceChildren prelude_btn [ txt ("↳ "^[%i"Hide"]) ] ; + Manip.SetCss.display prelude_container "" ; + Manip.SetCss.top iframe_pane "241px"; + set_arg "tab_text_prelude" "shown" + end else begin + Manip.replaceChildren prelude_btn [ txt ("↰ "^[%i"Show"]) ] ; + Manip.SetCss.display prelude_container "none" ; + Manip.SetCss.top iframe_pane "90px"; + set_arg "tab_text_prelude" "hidden" + end in + update () ; + Manip.Ev.onclick prelude_btn + (fun _ -> state := not !state ; update () ; true) ; + Manip.appendChildren prelude_pane + [ prelude_title ; prelude_container ] + let setup_prelude_pane ace prelude = if prelude = "" then () else let editor_pane = find_component "learnocaml-exo-editor-pane" in @@ -1115,7 +1151,7 @@ module Display_exercise = else ignore (onclick cid); Manip.removeChildren exp; - Manip.appendChild exp (txt (if !displayed then "[-]" else "[+]")); + Manip.appendChild exp (txt (if !displayed then "[+]" else "[-]")); displayed := not !displayed; true in diff --git a/src/app/learnocaml_common.mli b/src/app/learnocaml_common.mli index 275f2c8cf..f0d3b994c 100644 --- a/src/app/learnocaml_common.mli +++ b/src/app/learnocaml_common.mli @@ -143,7 +143,7 @@ val stars_div: float -> [> Html_types.div ] Tyxml_js.Html5.elt (** Returns an HTML string expected to be put in an iframe *) val exercise_text: - Exercise.Meta.t -> Exercise.t -> string + Exercise.Meta.t -> Learnocaml_exercise.t -> string val string_of_exercise_kind: Exercise.Meta.kind -> string @@ -221,6 +221,8 @@ val typecheck : val set_nickname_div : unit -> unit +val setup_tab_text_prelude_pane : string -> unit + val setup_prelude_pane : 'a Ace.editor -> string -> unit val get_token : ?has_server:bool -> unit -> Learnocaml_data.student Learnocaml_data.token option Lwt.t diff --git a/src/app/learnocaml_description_main.ml b/src/app/learnocaml_description_main.ml index 8ee7bcbee..6353ac8b5 100644 --- a/src/app/learnocaml_description_main.ml +++ b/src/app/learnocaml_description_main.ml @@ -15,10 +15,10 @@ module Exercise_link = a_class cl ] content) end - -module Display = Display_exercise(Exercise_link) + +module Display = Display_exercise(Exercise_link) open Display - + let () = run_async_with_log @@ fun () -> let id = match Url.Current.path with @@ -33,12 +33,24 @@ let () = retrieve (Learnocaml_api.Exercise (Some token, id)) in init_tabs (); - exercise_fetch >>= fun (ex_meta, exo, _deadline) -> - (* display exercise questions *) + exercise_fetch >>= fun (ex_meta, ex, _deadline) -> + let exo = match ex with + | Learnocaml_exercise.Subexercise (ex,_) -> + (match ex with + | [] -> raise Not_found + | (ex1,_) :: _ -> Learnocaml_exercise.Exercise ex1) + | ex -> ex + in + (* display exercise questions and prelude *) + setup_tab_text_prelude_pane Learnocaml_exercise.(decipher false File.prelude exo); + let prelude_container = find_component "learnocaml-exo-tab-text-prelude" in + let iframe_container = find_component "learnocaml-exo-tab-text-iframe" in let text_iframe = Dom_html.createIframe Dom_html.document in + Manip.replaceChildren iframe_container [Tyxml_js.Of_dom.of_iFrame text_iframe]; Manip.replaceChildren text_container - Tyxml_js.Html5.[ h1 [ txt ex_meta.title ] ; - Tyxml_js.Of_dom.of_iFrame text_iframe ] ; + Tyxml_js.Html5.[ h1 [ txt ex_meta.title] ; + prelude_container; + iframe_container ] ; Js.Opt.case (text_iframe##.contentDocument) (fun () -> failwith "cannot edit iframe document") diff --git a/src/app/learnocaml_exercise_main.ml b/src/app/learnocaml_exercise_main.ml index db13a77a1..ff0ffa482 100644 --- a/src/app/learnocaml_exercise_main.ml +++ b/src/app/learnocaml_exercise_main.ml @@ -47,7 +47,7 @@ let display_report exo report = Manip.removeClass report_button "failure" ; Manip.removeClass report_button "partial" ; let grade = - let max = Learnocaml_exercise.(access File.max_score exo) in + let max = Learnocaml_exercise.(access false File.max_score exo) in if max = 0 then 999 else score * 100 / max in if grade >= 100 then begin @@ -121,7 +121,14 @@ let () = in let after_init top = exercise_fetch >>= fun (_meta, exo, _deadline) -> - begin match Learnocaml_exercise.(decipher File.prelude exo) with + let ex = match exo with + | Learnocaml_exercise.Exercise ex -> ex + | Learnocaml_exercise.Subexercise ([], _ ) -> raise Not_found + | Learnocaml_exercise.Subexercise ((ex, _) :: _ ,_) -> ex + in + let sub_id = ex.Learnocaml_exercise.id + in + begin match Learnocaml_exercise.(decipher ~subid:sub_id false File.prelude (Learnocaml_exercise.Exercise ex)) with | "" -> Lwt.return true | prelude -> Learnocaml_toplevel.load ~print_outcome:true top @@ -129,7 +136,7 @@ let () = prelude end >>= fun r1 -> Learnocaml_toplevel.load ~print_outcome:false top - (Learnocaml_exercise.(decipher File.prepare exo)) >>= fun r2 -> + (Learnocaml_exercise.(decipher ~subid:sub_id false File.prepare (Learnocaml_exercise.Exercise ex))) >>= fun r2 -> if not r1 || not r2 then failwith [%i"error in prelude"] ; Learnocaml_toplevel.set_checking_environment top >>= fun () -> Lwt.return () in @@ -142,6 +149,14 @@ let () = set_nickname_div (); toplevel_launch >>= fun top -> exercise_fetch >>= fun (ex_meta, exo, deadline) -> + let sub_id = + match exo with + | Learnocaml_exercise.Subexercise (exs,_) -> + (match exs with + | [] -> "" + | (ex,_subex) :: _ -> ex.Learnocaml_exercise.id) + | _ -> "" + in (match deadline with | None -> () | Some 0. -> make_readonly () @@ -156,7 +171,7 @@ let () = solution | { Answer.report = None ; solution ; _ } -> solution - | exception Not_found -> Learnocaml_exercise.(access File.template exo) in + | exception Not_found -> Learnocaml_exercise.(access ~subid:sub_id false File.template exo) in (* ---- details pane -------------------------------------------------- *) let load_meta () = Lwt.async (fun () -> @@ -179,13 +194,13 @@ let () = (* ---- editor pane --------------------------------------------------- *) let editor, ace = setup_editor solution in let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in - EB.cleanup (Learnocaml_exercise.(access File.template exo)); + EB.cleanup (Learnocaml_exercise.(access ~subid:sub_id false File.template exo)); EB.sync token id; EB.download id; EB.eval top select_tab; let typecheck = typecheck top ace editor in (*------------- prelude -----------------*) - setup_prelude_pane ace Learnocaml_exercise.(decipher File.prelude exo); + setup_prelude_pane ace Learnocaml_exercise.(decipher ~subid:sub_id false File.prelude exo); Js.Opt.case (text_iframe##.contentDocument) (fun () -> failwith "cannot edit iframe document") @@ -193,11 +208,62 @@ let () = d##open_; d##write (Js.string (exercise_text ex_meta exo)); d##close) ; + + (* ------------------- Subexercise navigation -------- *) + + let nav_available = match exo with + | Learnocaml_exercise.Exercise _ -> false + | Learnocaml_exercise.Subexercise _ -> true + in + (* Traitement du "sous-index" pour savoir si on peut naviguer *) + token >>= fun tok -> + retrieve (Learnocaml_api.Exercise_index tok) >>= fun (index,l) -> + let navigation_toolbar = find_component "learnocaml-exo-tab-navigation" in + let prev_and_next id = + let rec loop = function + | [] -> assert false + | [ _ ] (* assumes single id *) -> None, None + | (one, _) :: (two, _) :: _ when id = one -> None, Some two + | (one, _) :: (two, _) :: [] when id = two -> Some one, None + | (one, _) :: (two, _) :: (three, _) :: _ when id = two -> Some one, Some three + | _ :: rest -> loop rest + in loop [id,1] in + let prev_button_state = button_state () in + let next_button_state = button_state () in + begin match prev_and_next id with + | None, None -> + disable_button prev_button_state ; + disable_button next_button_state + | Some _, None -> + enable_button prev_button_state ; + disable_button next_button_state + | None, Some _ -> + disable_button prev_button_state ; + enable_button next_button_state + | Some _, Some _ -> + enable_button prev_button_state ; + enable_button next_button_state + end ; + let subtitle_field = Tyxml_js.Html5.(h4 ~a: [a_class ["learnocaml-exo-subtitle"]] + [txt id]) in + let button_next = find_component "learnocaml-exo-button-next" in + let button_prev = find_component "learnocaml-exo-button-prev" in + Manip.appendChild ~before: button_next navigation_toolbar subtitle_field ; + if nav_available then + (Manip.SetCss.display button_next ""; + Manip.SetCss.display button_prev ""; + ) + else + (Manip.SetCss.display button_next "none"; + Manip.SetCss.display button_prev "none"; + Manip.SetCss.width subtitle_field "100%"; + ); + (* ---- main toolbar -------------------------------------------------- *) let exo_toolbar = find_component "learnocaml-exo-toolbar" in let toolbar_button = button ~container: exo_toolbar ~theme: "light" in begin toolbar_button - ~icon: "list" [%i"Exercises"] @@ fun () -> + ~icon: "list" [%i"Exercises"] @@ fun () -> Dom_html.window##.location##assign (Js.string (api_server ^ "/index.html#activity=exercises")) ; Lwt.return () @@ -283,6 +349,11 @@ let () = Ace.focus ace ; typecheck true end ; + if nav_available then + begin toolbar_button + ~icon: "reload" [%i"AllGrade!"] @@ fun () -> + typecheck true + end; Window.onunload (fun _ev -> local_save ace id; true); (* ---- return -------------------------------------------------------- *) toplevel_launch >>= fun _ -> diff --git a/src/app/learnocaml_index_main.ml b/src/app/learnocaml_index_main.ml index cf67f896c..0e623ca3f 100644 --- a/src/app/learnocaml_index_main.ml +++ b/src/app/learnocaml_index_main.ml @@ -77,73 +77,131 @@ let exercises_tab token _ _ () = let open Tyxml_js.Html5 in match contents with | Exercise.Index.Exercises exercises -> - List.fold_left - (fun acc (exercise_id, meta_opt) -> - match meta_opt with None -> acc | Some meta -> - let {Exercise.Meta.kind; title; short_description; stars; _ } = - meta - in - let pct_init = - match SMap.find exercise_id all_exercise_states with - | exception Not_found -> None - | { Answer.grade ; _ } -> grade in - let pct_signal, pct_signal_set = React.S.create pct_init in - Learnocaml_local_storage.(listener (exercise_state exercise_id)) := - Some (function - | Some { Answer.grade ; _ } -> pct_signal_set grade - | None -> pct_signal_set None) ; - let pct_text_signal = - React.S.map - (function - | None -> "--" - | Some 0 -> "0%" - | Some pct -> string_of_int pct ^ "%") - pct_signal in - let time_left = match List.assoc_opt exercise_id deadlines with - | None -> "" - | Some 0. -> [%i"Exercise closed"] - | Some f -> Printf.sprintf [%if"Time left: %s"] - (string_of_seconds (int_of_float f)) - in - let status_classes_signal = - React.S.map - (function - | None -> [ "stats" ] - | Some 0 -> [ "stats" ; "failure" ] - | Some pct when pct >= 100 -> [ "stats" ; "success" ] - | Some _ -> [ "stats" ; "partial" ]) - pct_signal in - a ~a:[ a_href (get_url token "/exercises/" "exercise.html#id=" exercise_id) ; - a_class [ "exercise" ] ] [ - div ~a:[ a_class [ "descr" ] ] ( - h1 [ txt title ] :: - begin match short_description with - | None -> [] - | Some text -> [ txt text ] - end - ); - div ~a:[ a_class [ "time-left" ] ] [H.txt time_left]; - div ~a:[ Tyxml_js.R.Html5.a_class status_classes_signal ] [ - stars_div stars; - div ~a:[ a_class [ "length" ] ] [ - match kind with - | Exercise.Meta.Project -> txt [%i"project"] - | Exercise.Meta.Problem -> txt [%i"problem"] - | Exercise.Meta.Exercise -> txt [%i"exercise"] ] ; - div ~a:[ a_class [ "score" ] ] [ - Tyxml_js.R.Html5.txt pct_text_signal - ] - ] ] :: - acc) - acc exercises + List.fold_left + (fun acc (exercise_id, meta_opt, subindex_opt) -> + match meta_opt,subindex_opt with + | None, None -> acc + | Some meta, _ -> + let {Exercise.Meta.kind; title; short_description; stars; _ } = + meta + in + let pct_init = + match SMap.find exercise_id all_exercise_states with + | exception Not_found -> None + | { Answer.grade ; _ } -> grade in + let pct_signal, pct_signal_set = React.S.create pct_init in + Learnocaml_local_storage.(listener (exercise_state exercise_id)) := + Some (function + | Some { Answer.grade ; _ } -> pct_signal_set grade + | None -> pct_signal_set None) ; + let pct_text_signal = + React.S.map + (function + | None -> "--" + | Some 0 -> "0%" + | Some pct -> string_of_int pct ^ "%") + pct_signal in + let time_left = match List.assoc_opt exercise_id deadlines with + | None -> "" + | Some 0. -> [%i"Exercise closed"] + | Some f -> Printf.sprintf [%if"Time left: %s"] + (string_of_seconds (int_of_float f)) + in + let status_classes_signal = + React.S.map + (function + | None -> [ "stats" ] + | Some 0 -> [ "stats" ; "failure" ] + | Some pct when pct >= 100 -> [ "stats" ; "success" ] + | Some _ -> [ "stats" ; "partial" ]) + pct_signal in + a ~a:[ a_href (get_url token "/exercises/" "exercise.html#id=" exercise_id) ; + a_class [ "exercise" ] ] [ + div ~a:[ a_class [ "descr" ] ] ( + h1 [ txt title ] :: + begin match short_description with + | None -> [] + | Some text -> [ txt text ] + end + ); + div ~a:[ a_class [ "time-left" ] ] [H.txt time_left]; + div ~a:[ Tyxml_js.R.Html5.a_class status_classes_signal ] [ + stars_div stars; + div ~a:[ a_class [ "length" ] ] [ + match kind with + | Exercise.Meta.Project -> txt [%i"project"] + | Exercise.Meta.Problem -> txt [%i"problem"] + | Exercise.Meta.Exercise -> txt [%i"exercise"] ] ; + div ~a:[ a_class [ "score" ] ] [ + Tyxml_js.R.Html5.txt pct_text_signal + ] + ] ] :: + acc + | None, Some subindex -> + let {Exercise.Meta.kind; title; short_description; stars; _ } = + Learnocaml_data.Exercise.Subindex.to_meta subindex + in + let pct_init = + match SMap.find exercise_id all_exercise_states with + | exception Not_found -> None + | { Answer.grade ; _ } -> grade in + let pct_signal, pct_signal_set = React.S.create pct_init in + Learnocaml_local_storage.(listener (exercise_state exercise_id)) := + Some (function + | Some { Answer.grade ; _ } -> pct_signal_set grade + | None -> pct_signal_set None) ; + let pct_text_signal = + React.S.map + (function + | None -> "--" + | Some 0 -> "0%" + | Some pct -> string_of_int pct ^ "%") + pct_signal in + let time_left = match List.assoc_opt exercise_id deadlines with + | None -> "" + | Some 0. -> [%i"Exercise closed"] + | Some f -> Printf.sprintf [%if"Time left: %s"] + (string_of_seconds (int_of_float f)) + in + let status_classes_signal = + React.S.map + (function + | None -> [ "stats" ] + | Some 0 -> [ "stats" ; "failure" ] + | Some pct when pct >= 100 -> [ "stats" ; "success" ] + | Some _ -> [ "stats" ; "partial" ]) + pct_signal in + a ~a:[ a_href (get_url token "/exercises/" "exercise.html#id=" exercise_id) ; + a_class [ "exercise" ] ] [ + div ~a:[ a_class [ "descr" ] ] ( + h1 [ txt title ] :: + begin match short_description with + | None -> [] + | Some text -> [ txt text ] + end + ); + div ~a:[ a_class [ "time-left" ] ] [H.txt time_left]; + div ~a:[ Tyxml_js.R.Html5.a_class status_classes_signal ] [ + stars_div stars; + div ~a:[ a_class [ "length" ] ] [ + match kind with + | Exercise.Meta.Project -> txt [%i"project"] + | Exercise.Meta.Problem -> txt [%i"problem"] + | Exercise.Meta.Exercise -> txt [%i"exercise"] ] ; + div ~a:[ a_class [ "score" ] ] [ + Tyxml_js.R.Html5.txt pct_text_signal + ] + ] ] :: + acc) + acc exercises | Exercise.Index.Groups groups -> - let h = match lvl with 1 -> h1 | 2 -> h2 | _ -> h3 in - List.fold_left - (fun acc (_, Exercise.Index.{ title ; contents }) -> - format_contents (succ lvl) - (h ~a:[ a_class [ "pack" ] ] [ txt title ] :: acc) - contents) - acc groups in + let h = match lvl with 1 -> h1 | 2 -> h2 | _ -> h3 in + List.fold_left + (fun acc (_, Exercise.Index.{ title ; contents }) -> + format_contents (succ lvl) + (h ~a:[ a_class [ "pack" ] ] [ txt title ] :: acc) + contents) + acc groups in List.rev (format_contents 1 [] index) in let list_div = match format_exercise_list @@ -152,8 +210,8 @@ let exercises_tab token _ _ () = | [] -> H.div [H.txt [%i"No open exercises at the moment"]] | l -> H.div ~a:[H.a_id El.Dyn.exercise_list_id] l in - Manip.appendChild El.content list_div; - Lwt.return list_div + Manip.appendChild El.content list_div; + Lwt.return list_div let playground_tab token _ _ () = show_loading [%i"Loading playground"] @@ fun () -> diff --git a/src/app/learnocaml_student_view.ml b/src/app/learnocaml_student_view.ml index 6b116b628..0738c34e2 100644 --- a/src/app/learnocaml_student_view.ml +++ b/src/app/learnocaml_student_view.ml @@ -386,7 +386,7 @@ let display_report exo report = let report_button = El.Tabs.(report.btn) in restore_report_button (); let grade = - let max = Learnocaml_exercise.(access File.max_score exo) in + let max = Learnocaml_exercise.(access true File.max_score exo) in if max = 0 then 999 else score * 100 / max in if grade >= 100 then begin diff --git a/src/app/learnocaml_teacher_tab.ml b/src/app/learnocaml_teacher_tab.ml index db47f5eaa..af173437e 100644 --- a/src/app/learnocaml_teacher_tab.ml +++ b/src/app/learnocaml_teacher_tab.ml @@ -203,7 +203,7 @@ let rec teacher_tab token _select _params () = mk_table (group_level + 1) acc status g.Exercise.Index.contents) acc groups_list | Exercise.Index.Exercises exlist -> - List.fold_left (fun acc (id, meta) -> + List.fold_left (fun acc (id, meta, _subindex) -> let open_exercise_ () = let _win = window_open ("/exercises/"^id^"/") "_blank" in false @@ -300,7 +300,7 @@ let rec teacher_tab token _select _params () = empty && empty0, List.rev_append hidden hidden0) (true, []) groups_list | Exercise.Index.Exercises l -> - List.fold_left (fun (empty, hidden) (id, ex) -> + List.fold_left (fun (empty, hidden) (id, ex, _subindex) -> let elt = find_component (exercise_line_id id) in match ex with | Some ex when matches id ex -> @@ -484,7 +484,7 @@ let rec teacher_tab token _select _params () = (Manip.addClass anystudents_line "student_hidden"; [`Any]) in let hidden = - Token.Map.fold (fun tok std hidden-> + Token.Map.fold (fun tok std hidden -> let elt = find_component (student_line_id (`Token tok)) in if matches std then (Manip.removeClass elt "student_hidden"; diff --git a/src/grader/grader_cli.ml b/src/grader/grader_cli.ml index 709f1a641..dc777753b 100644 --- a/src/grader/grader_cli.ml +++ b/src/grader/grader_cli.ml @@ -47,13 +47,17 @@ let read_student_file exercise_dir path = else Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read -let grade ?(print_result=false) ?dirname meta exercise output_json = +let grade ?(check=None) ?(print_result=false) ?dirname meta exercise output_json = Lwt.catch (fun () -> - let code_to_grade = match !grade_student with + let code_to_grade = + match check with + | Some path -> read_student_file (Sys.getcwd ()) ( path ^ "/solution.ml") + | None -> + match !grade_student with | Some path -> read_student_file (Sys.getcwd ()) path | None -> - Lwt.return (Learnocaml_exercise.(decipher File.solution exercise)) in + Lwt.return (Learnocaml_exercise.(decipher true File.solution exercise)) in let callback = if !display_callback then Some (Printf.eprintf "[ %s ]%!\r\027[K") else None in let timeout = !individual_timeout in @@ -138,20 +142,20 @@ let grade ?(print_result=false) ?dirname meta exercise output_json = if failure then begin if print_result then Printf.eprintf "%-30s - Failure - %d points\n%!" - Learnocaml_exercise.(access File.id exercise) max; + Learnocaml_exercise.(access true File.id exercise) max; Lwt.return (Error max) end else begin if print_result then Printf.eprintf "%-30s - Success - %d points\n%!" - Learnocaml_exercise.(access File.id exercise) max; + Learnocaml_exercise.(access true File.id exercise) max; match output_json with | None -> Lwt.return (Ok ()) | Some json_file -> let json = Json_encoding.(construct (tup3 Learnocaml_data.Exercise.Meta.enc Learnocaml_exercise.encoding (option float))) - (meta, Learnocaml_exercise.(update File.max_score max exercise), None) + (meta, Learnocaml_exercise.Exercise (Learnocaml_exercise.(update File.max_score max exercise)), None) in let json = match json with | `A _ | `O _ as d -> d @@ -183,4 +187,4 @@ let grade_from_dir ?(print_result=false) exercise_dir output_json = | "" -> `O [] | s -> Ezjsonm.from_string s) |> Json_encoding.destruct Learnocaml_data.Exercise.Meta.enc in - grade ~print_result ~dirname:exercise_dir meta exo output_json + grade ~print_result ~dirname:exercise_dir meta (Learnocaml_exercise.Exercise exo) output_json diff --git a/src/grader/grader_cli.mli b/src/grader/grader_cli.mli index e66095ef7..37b55af96 100644 --- a/src/grader/grader_cli.mli +++ b/src/grader/grader_cli.mli @@ -39,7 +39,9 @@ val dump_dot: string option ref (** Runs the grading process *) val grade: - ?print_result:bool -> ?dirname:string -> Learnocaml_data.Exercise.Meta.t -> Learnocaml_exercise.t -> string option -> + ?check: string option -> ?print_result:bool -> ?dirname:string -> + Learnocaml_data.Exercise.Meta.t -> + Learnocaml_exercise.t -> string option -> (unit, int) result Lwt.t val grade_from_dir: diff --git a/src/grader/grading.ml b/src/grader/grading.ml index dce50a237..edfa48241 100644 --- a/src/grader/grading.ml +++ b/src/grader/grading.ml @@ -102,12 +102,12 @@ let get_grade set_progress [%i"Loading the prelude."] ; handle_error (internal_error [%i"while loading the prelude"]) @@ Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "prelude.ml") - (Learnocaml_exercise.(decipher File.prelude exo)) ; + (Learnocaml_exercise.(decipher false File.prelude exo)) ; set_progress [%i"Preparing the test environment."] ; handle_error (internal_error [%i"while preparing the tests"]) @@ Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "prepare.ml") - (Learnocaml_exercise.(decipher File.prepare exo)) ; + (Learnocaml_exercise.(decipher false File.prepare exo)) ; set_progress [%i"Loading your code."] ; handle_error user_code_error @@ @@ -117,7 +117,7 @@ let get_grade set_progress [%i"Loading the solution."] ; handle_error (internal_error [%i"while loading the solution"]) @@ Toploop_ext.use_mod_string ~print_outcome ~ppf_answer ~modname:"Solution" - (Learnocaml_exercise.(decipher File.solution exo)) ; + (Learnocaml_exercise.(decipher false File.solution exo)) ; set_progress [%i"Preparing to launch the tests."] ; Introspection.allow_introspection ~divert ; @@ -145,12 +145,12 @@ let get_grade let () = let open Learnocaml_exercise in - let files = File.dependencies (access File.depend exo) in + let files = File.dependencies (access false File.depend exo) in let rec load_dependencies signatures = function | [] -> () (* signatures without implementation are ignored *) | file::fs -> let path = File.key file - and content = decipher file exo in + and content = decipher false file exo in let modname = String.capitalize_ascii @@ Filename.remove_extension @@ Filename.basename path in match Filename.extension path with @@ -188,7 +188,7 @@ let get_grade handle_error (internal_error [%i"while testing your solution"]) @@ Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "test.ml") - (Learnocaml_exercise.(decipher File.test exo)) ; + (Learnocaml_exercise.(decipher false File.test exo)) ; (* Memory cleanup... *) Toploop.initialize_toplevel_env () ; diff --git a/src/main/learnocaml_client.ml b/src/main/learnocaml_client.ml index d4518988c..a4cad6bf9 100644 --- a/src/main/learnocaml_client.ml +++ b/src/main/learnocaml_client.ml @@ -348,7 +348,7 @@ let get_score = in get_score 0 -let max_score exo = Learnocaml_exercise.(access File.max_score exo) +let max_score exo = Learnocaml_exercise.(access false File.max_score exo) let print_score ?(max=1) ?color i = let color = match color with @@ -475,7 +475,7 @@ let upload_save server_url token save = let upload_report server token ex solution report = let score = get_score report in let max_score = max_score ex in - let id = Learnocaml_exercise.(access File.id ex) in + let id = Learnocaml_exercise.(access (Learnocaml_data.Token.is_student token) File.id ex) in let mtime = Unix.gettimeofday () in let exercise_state = { Answer. @@ -911,7 +911,7 @@ module Template = struct >>= fun (_meta, exercise, _deadline) -> write_exercise_file exercise_id - Learnocaml_exercise.(access File.template exercise) + Learnocaml_exercise.(access false File.template exercise) >|= function | true -> 0 | false -> 3 diff --git a/src/repo/learnocaml_exercise.ml b/src/repo/learnocaml_exercise.ml index 5157b05e0..d08170303 100644 --- a/src/repo/learnocaml_exercise.ml +++ b/src/repo/learnocaml_exercise.ml @@ -8,7 +8,19 @@ type id = string -type t = +type check_all_against = string option + +type subexercise = + { sub_id : id; + student_hidden : bool; + student_weight : int; + teacher_weight : int; + } + +let construct_subexercise sub_id student_hidden student_weight teacher_weight = + {sub_id; student_hidden; student_weight; teacher_weight} + +type exercise = { id : id ; prelude : string ; template : string ; @@ -21,24 +33,61 @@ type t = dependencies : string list; } + +type t = + | Subexercise of ((exercise * subexercise) list * check_all_against ) + | Exercise of exercise + let encoding = let open Json_encoding in - conv - (fun { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score ; depend ; dependencies} -> - id, prelude, template, descr, prepare, test, solution, max_score,depend, dependencies) - (fun (id, prelude, template, descr, prepare, test, solution, max_score,depend, dependencies) -> - { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score ; depend ; dependencies}) - (obj10 - (req "id" string) - (req "prelude" string) - (req "template" string) - (req "descr" (list (tup2 string string))) - (req "prepare" string) - (req "test" string) - (req "solution" string) - (req "max-score" int) - (req "depend" (option string)) - (req "dependencies" (list string))) + let exercise_enc = + conv + (fun { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score ; depend ; dependencies} -> + id, prelude, template, descr, prepare, test, solution, max_score,depend, dependencies) + (fun (id, prelude, template, descr, prepare, test, solution, max_score,depend, dependencies) -> + { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score ; depend ; dependencies}) + (obj10 + (req "id" string) + (req "prelude" string) + (req "template" string) + (req "descr" (list (tup2 string string))) + (req "prepare" string) + (req "test" string) + (req "solution" string) + (req "max-score" int) + (req "depend" (option string)) + (req "dependencies" (list string))) + in + let sub_enc = + conv + (fun { sub_id ; student_hidden ; student_weight ; teacher_weight } -> + sub_id, student_hidden, student_weight, teacher_weight) + (fun (sub_id, student_hidden, student_weight, teacher_weight) -> + { sub_id ; student_hidden ; student_weight ; teacher_weight }) + (obj4 + (req "id" string) + (req "student_hidden" bool) + (req "student_weight" int) + (req "teacher_weight" int)) + in + let subexercise_enc = + obj1 + (req "subexercise" (tup2 (list (tup2 exercise_enc sub_enc)) (obj1 (opt "check_all_against" string)))) + in + union + [case + exercise_enc + (function + |Exercise map -> Some map + |_ -> None) + (fun map -> Exercise map ); + case + subexercise_enc + (function + | Subexercise map -> Some map + | _ -> None) + (fun map -> Subexercise map) + ] (* let meta_from_string m = * Ezjsonm.from_string m @@ -88,8 +137,8 @@ module File = struct ciphered : bool ; decode : string -> 'a ; encode : 'a -> string ; - field : t -> 'a ; - update : 'a -> t -> t ; + field : exercise -> 'a ; + update : 'a -> exercise -> exercise ; } exception Missing_file of string @@ -106,9 +155,9 @@ module File = struct with Not_found -> raise (Missing_file ("get " ^ key)) let get_opt file ex = - try (* a missing file here is necessarily [file] *) - get file ex - with Missing_file _ -> None + try (* a missing file here is necessarily [file] *) + get file ex + with Missing_file _ -> None let has { key ; _ } ex = StringMap.mem key ex @@ -350,37 +399,62 @@ module File = struct include MakeReader (Seq) end -let access f ex = - f.File.field ex - -let decipher f ex = +let access ?(subid="") _is_Student f ex = + match ex with + | Exercise exo -> f.File.field exo + | Subexercise (subexos,_) -> + f.File.field @@ + (fun (ex,_) -> ex) @@ + List.find + (fun (ex,_subex) -> ex.id = subid) + subexos + +let decipher ?(subid="") _is_Student f ex = + let exo = match ex with + | Exercise exo -> exo + | Subexercise (subexos,_) -> + (fun (ex,_) -> ex) @@ + List.find + (fun (ex,_subex) -> ex.id = subid) + subexos + in let open File in - let raw = f.field ex in + let raw = f.field exo in if f.ciphered then let prefix = - Digest.string (ex.id ^ "_" ^ f.key) in + Digest.string (exo.id ^ "_" ^ f.key) in f.decode (Learnocaml_xor.decode ~prefix raw) else f.decode raw -let update f v ex = - f.File.update v ex - -let cipher f v ex = +let update ?(subid="") f v ex = + let exo = match ex with + | Exercise exo -> exo + | Subexercise (subexos, _) -> (fun (ex,_) -> ex) @@ + List.find (fun (ex,_) -> ex.id = subid) subexos + in + f.File.update v exo + +let cipher ?(subid="") f v ex = + let exo = match ex with + | Exercise exo -> exo + | Subexercise (subexos, _) -> (fun (ex,_) -> ex) @@ + List.find (fun (ex, _) -> ex.id = subid) subexos + in let open File in if f.ciphered then let prefix = - Digest.string (ex.id ^ "_" ^ f.key) in - f.update (Learnocaml_xor.encode ~prefix (f.encode v)) ex + Digest.string (exo.id ^ "_" ^ f.key) in + f.update (Learnocaml_xor.encode ~prefix (f.encode v)) exo else - f.update (f.encode v) ex + f.update (f.encode v) exo let field_from_file file files = try File.(StringMap.find file.key files |> file.decode) with Not_found -> raise File.(Missing_file file.key) module MakeReaderAnddWriter (Concur : Concur) = struct - + module FileReader = File.MakeReader(Concur) let read ~read_field ?id ?decipher () = @@ -399,19 +473,19 @@ module MakeReaderAnddWriter (Concur : Concur) = struct solution = field_from_file File.solution ex ; max_score = 0 ; depend ; - dependencies = + dependencies = let field_from_dependency file = try field_from_file file ex - with File.Missing_file msg - -> let msg' = msg ^ ": dependency declared in " + with File.Missing_file msg + -> let msg' = msg ^ ": dependency declared in " ^ File.(key depend) ^ ", but not found" in - raise (File.Missing_file msg') - in - List.map field_from_dependency (File.dependencies depend) + raise (File.Missing_file msg') + in + List.map field_from_dependency (File.dependencies depend) } with File.Missing_file _ as e -> fail e - let write ~write_field ex ?(cipher = true) acc = + let write ~write_field ex ?(cipher = true) is_Student acc = let open Concur in let open File in let acc = ref acc in @@ -441,7 +515,7 @@ module MakeReaderAnddWriter (Concur : Concur) = struct write_field test ; write_field depend ; (* write_field max_score *) ] - @ (List.map write_field (dependencies (access depend ex))) ) + @ (List.map write_field (dependencies (access is_Student depend (Exercise ex) ))) ) >>= fun () -> return !acc end diff --git a/src/repo/learnocaml_exercise.mli b/src/repo/learnocaml_exercise.mli index 1bb8809a7..c3a057799 100644 --- a/src/repo/learnocaml_exercise.mli +++ b/src/repo/learnocaml_exercise.mli @@ -9,10 +9,38 @@ (** Internal representation of the exercises files, including the metadata from the repository. *) -type t + type id = string +type check_all_against = string option + +type subexercise = + { sub_id : id; + student_hidden : bool; + student_weight : int; + teacher_weight : int; + } + +val construct_subexercise : id -> bool -> int -> int -> subexercise + +type exercise = { + id : id; + prelude : string; + template : string; + descr : (string * string) list; + prepare : string; + test : string; + solution : string; + max_score : int; + depend : string option; + dependencies : string list; + } + +type t = + |Subexercise of ((exercise * subexercise) list * check_all_against ) + | Exercise of exercise + (* JSON encoding of the exercise representation. Includes cipher and decipher at at encoding and decoding. *) val encoding: t Json_encoding.encoding @@ -82,43 +110,47 @@ end (** Access a field from the exercise, using the [t] representation, without ** deciphering it. May raise [Missing_file] if the field is optional and set to - [None]. *) -val access: 'a File.file -> t -> 'a + [None]. + For subexercises, cannot access if the users is a student and the subexercise + is student_hidden. It will raise a Not_found exeption *) +val access: ?subid:id -> bool -> 'a File.file -> t -> 'a (** Access a string field from the exercise, using the [t] representation, and deciphers if necessary. May raise [Missing_file] if the field is optional and - set to [None]. *) -val decipher: string File.file -> t -> string + set to [None]. + For subexercises, cannot access and decipher if the users is a student and the subexercise + is student_hidden. It will raise a Not_found exeption *) +val decipher: ?subid:id -> bool -> string File.file -> t -> string (** Updates the value of a field of the exercise in its [t] representation. *) -val update: 'a File.file -> 'a -> t -> t +val update: ?subid:id -> 'a File.file -> 'a -> t -> exercise (** Updates the value of a field of the exercise in its [t] representation, and ciphers it. *) -val cipher: string File.file -> string -> t -> t +val cipher: ?subid:id -> string File.file -> string -> t -> exercise (** Reader and decipherer *) val read: read_field:(string -> string option) -> ?id:string -> ?decipher:bool -> unit -> - t + exercise (** Writer and cipherer, ['a] can be [unit] *) val write: write_field:(string -> string -> 'a -> 'a) -> - t -> ?cipher:bool -> 'a -> + exercise -> ?cipher:bool -> bool -> 'a -> 'a (** Reader and decipherer with {!Lwt} *) val read_lwt: read_field:(string -> string option Lwt.t) -> ?id:string -> ?decipher:bool -> unit -> - t Lwt.t + exercise Lwt.t (** Writer and cipherer with {!Lwt}, ['a] can be [unit] *) val write_lwt: write_field:(string -> string -> 'a -> 'a Lwt.t) -> - t -> ?cipher:bool -> 'a -> + exercise -> ?cipher:bool -> bool -> 'a -> 'a Lwt.t (** JSON serializer, with {!id} file included *) diff --git a/src/repo/learnocaml_process_exercise_repository.ml b/src/repo/learnocaml_process_exercise_repository.ml index df2cbb627..fe703e11a 100644 --- a/src/repo/learnocaml_process_exercise_repository.ml +++ b/src/repo/learnocaml_process_exercise_repository.ml @@ -52,7 +52,7 @@ let print_grader_error exercise = function | Error (-1) -> () | Error n -> Format.eprintf "[ERROR] %s: the solution has errors! (%d points%s)@." - Learnocaml_exercise.(access File.id exercise) + Learnocaml_exercise.(access true File.id exercise) n (if !Grader_cli.display_reports then "" else ". Run with '-v' to see the report") @@ -76,21 +76,42 @@ let spawn_grader Grader_cli.display_callback := false; Lwt_main.run (Lwt.catch (fun () -> - Grader_cli.grade ?print_result ?dirname meta exercise output_json - >|= fun r -> - print_grader_error exercise r; - match r with - | Ok () -> exit 0 - | Error _ -> exit 1) - (fun e -> - Printf.eprintf "%!Grader error: %s\n%!" (Printexc.to_string e); - exit 10)) + (match exercise with + | Learnocaml_exercise.Subexercise (exs, _check_all_against) -> + (* match check_all_against with + | Some id -> + let exo = // Ici : trouver l'exercice du CAA + Lwt_list.map_p // Ici : noter tout à partir du CAA + | None -> *) + Lwt_list.map_p + (fun (exo,_subexs) -> Grader_cli.grade ?print_result ?dirname meta + (Learnocaml_exercise.Exercise exo) output_json) + exs + | exo -> Lwt_list.map_p + (fun exo -> Grader_cli.grade ?print_result ?dirname meta + exo output_json) + [exo] + ) + >|= fun l -> + let rec aux = function + | [] -> [] + | r :: l -> ( print_grader_error exercise r; r :: aux l) + in + let rec result = function + | Ok () :: l -> result l + | Error _ :: _ -> exit 1 + | [] -> exit 0 + in + result @@ aux l) + (fun e -> + Printf.eprintf "%!Grader error: %s\n%!" (Printexc.to_string e); + exit 10)) | pid -> Lwt_unix.waitpid [] pid >>= fun (_pid, ret) -> incr n_processes; match ret with - | Unix.WEXITED 0 -> Lwt.return (Ok ()) - | _ -> Lwt.return (Error (-1)) + | Unix.WEXITED 0 -> Lwt.return ([Ok ()]) + | _ -> Lwt.return ([Error (-1)]) let main dest_dir = let exercises_index = @@ -115,9 +136,15 @@ let main dest_dir = in if Sys.file_exists (f / "meta.json") then match acc with - | None -> Some (Index.Exercises [full_id, None]) + | None -> Some (Index.Exercises [full_id, None, None]) | Some (Index.Exercises e) -> - Some (Index.Exercises (e @ [full_id, None])) + Some (Index.Exercises (e @ [full_id, None, None])) + | _ -> None + else if Sys.file_exists (f / "subindex.json") then + match acc with + | None -> Some (Index.Exercises [full_id, None, None]) + | Some (Index.Exercises e) -> + Some (Index.Exercises (e @ [full_id, None, None])) | _ -> None else if Sys.is_directory f then match acc, auto_index f with @@ -144,10 +171,10 @@ let main dest_dir = Format.eprintf "This does not look like a LearnOCaml exercise repository.@." ; Lwt.fail (Failure "cannot continue"))) >>= fun structure -> - (* Exercises must be unique, since their id refer to the directory. *) let rec fill_structure all_exercises = function | Index.Groups groups -> + print_string "fill_structure groups\n"; (* Ensures groups of a same parent are unique *) Lwt_list.fold_left_s (fun (all_exercises, subgroups, acc) (id, gr) -> @@ -163,30 +190,72 @@ let main dest_dir = >|= fun (all_exercises, _subgroups, groups) -> all_exercises, Index.Groups groups | Index.Exercises ids -> + print_string "fillstructure exe\n"; let filtered id = !exercises_filtered <> SSet.empty && not (SSet.mem id !exercises_filtered) in Lwt_list.fold_left_s - (fun (all_exercises, acc) (id, _) -> + (fun (all_exercises, acc) (id, _ , _) -> if SMap.mem id all_exercises || filtered id then Lwt.return (all_exercises, acc) else - from_file Meta.enc - (!exercises_dir / id / "meta.json") - >>= fun meta -> - read_exercise (!exercises_dir / id) - >|= fun exercise -> - SMap.add id exercise all_exercises, - (id, Some meta) :: acc) + if Sys.file_exists (!exercises_dir / id / "meta.json") then + (from_file Meta.enc + (!exercises_dir / id / "meta.json") + >>= fun meta -> + read_exercise (!exercises_dir / id) + >|= fun exercise -> + SMap.add id (Learnocaml_exercise.Exercise exercise) all_exercises, + (id, Some meta, None) :: acc) + else + (from_file (Subindex.enc) + (!exercises_dir / id / "subindex.json") + >>= fun meta -> + let check_all_against = + match Exercise.Subindex.to_check meta with + | Some check -> Some (!exercises_dir / id / check) + | _ -> None + in + let subexercise_list = Exercise.Subindex.to_part meta + in + let rec aux = function + | [] -> [] + | part::l -> + let (_,subexercise,student_hidden,s_weight,t_weight) = + Exercise.Subindex.get_part_field part + in (id,subexercise,student_hidden,s_weight,t_weight,Some meta)::aux l + in + let listing = aux subexercise_list + in + let subexercises = + (Lwt_list.fold_left_s + (fun (acc) (sup_id,sub_id,s_hidden,s_weight,t_weight,_) -> + let long_id = (sup_id / sub_id) in + if SMap.mem long_id all_exercises || filtered long_id then + Lwt.return acc + else + read_exercise (!exercises_dir / long_id) + >|= fun exercise -> + print_string (long_id^"\n"); + let subexercise = Learnocaml_exercise.construct_subexercise long_id s_hidden s_weight t_weight + in + (exercise,subexercise) :: acc)) + ([]) (List.rev listing) + in subexercises >|= fun exercise -> + SMap.add id + (Learnocaml_exercise.Subexercise (exercise, check_all_against)) all_exercises, + (id, None, Some meta) :: acc)) (all_exercises, []) (List.rev ids) >>= fun (all_exercises, exercises) -> Lwt.return (all_exercises, Index.Exercises exercises) in fill_structure SMap.empty structure >>= fun (all_exercises, index) -> - to_file Index.enc (dest_dir / Learnocaml_index.exercise_index_path) index >>= fun () -> + to_file Index.enc (dest_dir / Learnocaml_index.exercise_index_path) + index >>= fun () -> dump_dot index >>= fun () -> Learnocaml_store.Exercise.Index.get_from_index index >>= fun index -> - to_file Json_encoding.(tup2 Learnocaml_store.Exercise.Index.enc (assoc float)) (dest_dir / "exercise-index.json") (index, []) + to_file Json_encoding.(tup2 Learnocaml_store.Exercise.Index.enc (assoc float)) + (dest_dir / "exercise-index.json") (index, []) >>= fun () -> SSet.iter (fun id -> if not (SMap.mem id all_exercises) then @@ -195,72 +264,131 @@ let main dest_dir = let processes_arguments = List.rev @@ SMap.fold - (fun id exercise acc -> - let exercise_dir = !exercises_dir / id in - let json_path = dest_dir / Learnocaml_index.exercise_path id in - let changed = try - let { Unix.st_mtime = json_time ; _ } = Unix.stat json_path in - Sys.readdir exercise_dir |> - Array.to_list |> - List.map (fun f -> (Unix.stat (exercise_dir / f)).Unix.st_mtime ) |> - List.exists (fun t -> t >= json_time) - with _ -> true in - let dump_outputs = - match !dump_outputs with - | None -> None - | Some dir -> Some (dir / id) in - let dump_reports = - match !dump_reports with - | None -> None - | Some dir -> Some (dir / id) in - (id, exercise_dir, exercise, json_path, - changed, dump_outputs, dump_reports) :: acc) + (fun id exercise acc -> + match exercise with + | Learnocaml_exercise.Exercise _ -> + let exercise_dir = !exercises_dir / id in + let json_path = dest_dir / Learnocaml_index.exercise_path id in + let changed = try + let { Unix.st_mtime = json_time ; _ } = Unix.stat json_path in + Sys.readdir exercise_dir |> + Array.to_list |> + List.map (fun f -> (Unix.stat (exercise_dir / f)).Unix.st_mtime ) |> + List.exists (fun t -> t >= json_time) + with _ -> true in + let dump_outputs = + match !dump_outputs with + | None -> None + | Some dir -> Some (dir / id) in + let dump_reports = + match !dump_reports with + | None -> None + | Some dir -> Some (dir / id) in + (id, exercise_dir, exercise, json_path, + changed, dump_outputs, dump_reports) :: acc + | Learnocaml_exercise.Subexercise (ex,_) -> + List.append (List.rev @@ List.fold_right + (fun (exo,_) acc -> + let sub_id = exo.Learnocaml_exercise.id + in + let exercise_dir = !exercises_dir / id / sub_id in + let json_path = dest_dir / Learnocaml_index.exercise_path (id / sub_id) in + let changed = try + let { Unix.st_mtime = json_time ; _ } = Unix.stat json_path in + Sys.readdir exercise_dir |> + Array.to_list |> + List.map (fun f -> (Unix.stat (exercise_dir / f)).Unix.st_mtime ) |> + List.exists (fun t -> t >= json_time) + with _ -> true in + let dump_outputs = + match !dump_outputs with + | None -> None + | Some dir -> Some (dir / id ) in + let dump_reports = + match !dump_reports with + | None -> None + | Some dir -> Some (dir / id ) in + (id, exercise_dir, exercise, json_path, + changed, dump_outputs, dump_reports) :: acc) + ex []) acc) all_exercises [] in begin let listmap, grade = if !n_processes = 1 then - Lwt_list.map_s, + (Lwt_list.map_s, fun dump_outputs dump_reports ?print_result ?dirname - meta exercise json_path -> - Grader_cli.dump_outputs := dump_outputs; - Grader_cli.dump_reports := dump_reports; - Grader_cli.grade ?print_result ?dirname meta exercise json_path - >|= fun r -> print_grader_error exercise r; r + meta exercise json_path -> + Grader_cli.dump_outputs := dump_outputs; + Grader_cli.dump_reports := dump_reports; + (match exercise with + | Learnocaml_exercise.Subexercise (exs,check_all_against) -> + (match check_all_against with + | Some _ -> + Lwt_list.map_p + (fun (exo,_) -> + Grader_cli.grade ~check:check_all_against ?print_result ?dirname meta + (Learnocaml_exercise.Exercise exo) json_path) + exs + >>= fun check_all_against_result -> + (Lwt_list.map_p + (fun (exo,_) -> Grader_cli.grade ?print_result ?dirname meta + (Learnocaml_exercise.Exercise exo) json_path) + exs) + >>= fun normal_result -> + Lwt.return @@ List.append check_all_against_result normal_result + | None -> + Lwt_list.map_p + (fun (exo,_) -> Grader_cli.grade ?print_result ?dirname meta + (Learnocaml_exercise.Exercise exo) json_path) + exs) + | exo -> Lwt_list.map_p + (fun exo -> Grader_cli.grade ?print_result ?dirname meta + exo json_path) + [exo] + ) + >|= fun l -> + let rec aux = function + | [] -> [] + | r :: l -> ( print_grader_error exercise r; r :: aux l) + in aux l ) else Lwt_list.map_p, spawn_grader in listmap (fun (id, ex_dir, exercise, json_path, changed, dump_outputs,dump_reports) -> + print_string ("ID : "^id^"\nEx_dir : "^ex_dir^"\n"); let dst_ex_dir = String.concat Filename.dir_sep [dest_dir; "static"; id] in Lwt_utils.mkdir_p dst_ex_dir >>= fun () -> - Lwt_stream.iter_p (fun base -> + Lwt_stream.iter_p (fun base -> let d = Filename.concat ex_dir base in let dst = String.concat Filename.dir_sep [dst_ex_dir; base] in if Sys.is_directory d && base.[0] <> '.' then - Lwt_utils.copy_tree d dst + Lwt_utils.copy_tree d dst else Lwt.return_unit) - (Lwt_unix.files_of_directory ex_dir) >>= fun () -> - if not changed then begin + (Lwt_unix.files_of_directory ex_dir) >>= fun () -> + if not changed then begin Format.printf "%-24s (no changes)@." id ; Lwt.return true end else begin grade dump_outputs dump_reports ~dirname:(!exercises_dir / id) (Index.find index id) exercise (Some json_path) - >>= function - | Ok () -> - Format.printf "%-24s [OK]@." id ; - Lwt.return true - | Error _ -> - Format.printf "%-24s [FAILED]@." id ; - Lwt.return false + >>= + function + | Ok () :: _ (* à changer *) -> + Format.printf "%-24s [OK]@." id ; + Lwt.return true + | Error _ :: _-> + Format.printf "%-24s [FAILED]@." id ; + Lwt.return false + | [] -> Lwt.return false end) - processes_arguments + processes_arguments end >>= fun results -> Lwt.return (List.for_all ((=) true) results)) (fun exn -> - let print_unknown ppf = function - | Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg - | exn -> Format.fprintf ppf "Cannot process exercises: %s" (Printexc.to_string exn) in - Json_encoding.print_error ~print_unknown Format.err_formatter exn ; - Format.eprintf "@." ; - Lwt.return false) + let print_unknown ppf = function + | Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg + | exn -> Format.fprintf ppf "Cannot process exercises: %s" (Printexc.to_string exn) in + Json_encoding.print_error ~print_unknown Format.err_formatter exn ; + Format.eprintf "@." ; + Lwt.return false) diff --git a/src/repo/learnocaml_process_playground_repository.ml b/src/repo/learnocaml_process_playground_repository.ml index d73f2d808..4a1239aa3 100644 --- a/src/repo/learnocaml_process_playground_repository.ml +++ b/src/repo/learnocaml_process_playground_repository.ml @@ -16,8 +16,8 @@ let playground_index = ref None let errored exn = let print_unknown ppf = function - | Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg - | exn -> Format.fprintf ppf "Cannot process exercises: %s" (Printexc.to_string exn) in + | Failure msg -> Format.fprintf ppf "Cannot process exercises (playground): %s" msg + | exn -> Format.fprintf ppf "Cannot process exercises (playground): %s" (Printexc.to_string exn) in Json_encoding.print_error ~print_unknown Format.err_formatter exn ; Format.eprintf "@." ; Lwt.return false diff --git a/src/server/learnocaml_server.ml b/src/server/learnocaml_server.ml index bcb01291d..d9c3ce125 100644 --- a/src/server/learnocaml_server.ml +++ b/src/server/learnocaml_server.ml @@ -172,7 +172,19 @@ let log conn api_req = flush oc let check_report exo report grade = - let max_grade = Learnocaml_exercise.(access File.max_score) exo in + let max_grade = match exo with + | Learnocaml_exercise.Subexercise (subexs,_) -> + let rec aux acc = function + | [] -> 0 + | (ex,subex) :: l -> + let open Learnocaml_exercise in + aux (acc + subex.student_weight * + (access true File.max_score (Exercise ex))) + l + in + aux 0 subexs + | ex -> Learnocaml_exercise.(access true File.max_score) ex + in let score, _ = Learnocaml_report.result report in score * 100 / max_grade = grade diff --git a/src/state/learnocaml_data.ml b/src/state/learnocaml_data.ml index 39ce6f6d0..9a7f32acb 100644 --- a/src/state/learnocaml_data.ml +++ b/src/state/learnocaml_data.ml @@ -457,8 +457,8 @@ module Exercise = struct in let exercise_enc_v2 = J.(obj1 - (opt "max_score" int)) - (* deprecated & ignored *) + (opt "max_score" int) (* deprecated & ignored *) + ) in J.conv (fun t -> @@ -478,6 +478,115 @@ module Exercise = struct end + module Subindex = struct + + type meta = Meta.t + + type part = { + subtitle: string; + subexercise: string; + student_hidden: bool; + student_weight: int; + teacher_weight: int + } + + type t = { + meta : meta; + check_all_against: string option; + parts: part list; + } + + let to_meta s = s.meta + + let to_check s = s.check_all_against + + let to_part s = s.parts + + let get_part_field p = + let + {subtitle; subexercise; student_hidden; + student_weight; teacher_weight} = p + in (subtitle, subexercise, student_hidden, + student_weight, teacher_weight) + + let to_subindex m c p = + let meta = m in + let check_all_against = c in + let parts = p in + {meta ; check_all_against ; parts} + + let enc = + let meta_enc = Meta.enc + in + let part_enc = + J.(obj5 + (req "subtitle" string) + (req "subexercise" string) + (dft "student_hidden" bool false) + (dft "student_weight" int 1) + (dft "teacher_weight" int 1)) + in + let exercise_enc = + J.(obj3 + (req "meta" meta_enc) + (opt "check_all_against" string) + (req "parts" (list part_enc))) + in + J.conv + (fun {meta; check_all_against; parts} -> + let parts = + let rec aux = function + | [] -> [] + | {subtitle; subexercise; student_hidden; + student_weight; teacher_weight} :: l -> + (subtitle, subexercise, student_hidden, + student_weight, teacher_weight) :: (aux l) + in aux parts + in + (meta, + check_all_against, + parts)) + (fun (meta,check_all_against, part) -> + let parts = + let rec aux = function + | [] -> [] + | (subtitle, subexercise, student_hidden, + student_weight, teacher_weight) :: l -> + {subtitle; subexercise; student_hidden; + student_weight; teacher_weight} :: (aux l) + in aux part + in + to_subindex meta check_all_against parts) + (enc_check_version_1 (exercise_enc)) + + + let find t id = + let rec aux = function + | [] -> raise Not_found + | {subtitle; subexercise; student_hidden; + student_weight; teacher_weight}::l-> + if id = subtitle then + {subtitle; subexercise; student_hidden; + student_weight; teacher_weight} + else + aux l + in + aux (to_part t) + + let find_opt t id = try Some (find t id) with Not_found -> None + + let map_exercises f l = + (List.map (function + | (id, Some ex) -> + (id, Some (to_subindex + (to_meta ex) + (to_check ex) + (f l (to_part ex)))) + | x -> x) + l) + + end + module Status = struct type skill = [`Plus | `Minus] * string @@ -711,7 +820,7 @@ module Exercise = struct module Index = struct type t = - | Exercises of (id * Meta.t option) list + | Exercises of (id * Meta.t option * Subindex.t option) list | Groups of (string * group) list and group = { title : string; @@ -721,11 +830,14 @@ module Exercise = struct let exercise_enc = J.union [ J.case J.string - (function id, None -> Some id | _ -> None) - (fun id -> id, None); + (function id, None, None -> Some id | _ -> None) + (fun id -> id, None, None); J.case J.(tup2 string Meta.enc) - (function id, Some meta -> Some (id, meta) | _ -> None) - (fun (id, meta) -> id, Some meta); + (function id, Some meta, None -> Some (id, meta) | _ -> None) + (fun (id, meta) -> id, Some meta, None); + J.case J.(tup2 string Subindex.enc) + (function id, None, Some submeta -> Some (id, submeta) | _ -> None) + (fun (id, submeta) -> id, None, Some submeta); ] in let group_enc = @@ -739,17 +851,18 @@ module Exercise = struct (req "title" string) (req "exercises" (list exercise_enc))) (function - | (title, Exercises map) -> Some (title, map) - | _ -> None) + | (title, Exercises map) -> Some (title, map) + | _ -> None) (fun (title, map) -> (title, Exercises map)) ; J.case J.(obj2 (req "title" string) (req "groups" (assoc group_enc))) (function - | (title, Groups map) -> Some (title, map) - | _ -> None) - (fun (title, map) -> (title, Groups map)) ] + | (title, Groups map) -> Some (title, map) + | _ -> None) + (fun (title, map) -> (title, Groups map)) + ] in enc_check_version_2 @@ J.union @@ -771,9 +884,16 @@ module Exercise = struct | Groups ((_, g)::r) -> (try aux g.contents with Not_found -> aux (Groups r)) | Groups [] -> raise Not_found - | Exercises l -> (match List.assoc id l with - | None -> raise Not_found - | Some e -> e) + | Exercises l -> + let rec assoc_tup3 id = function + | [] -> raise Not_found + | (ex_id, meta, subindex) :: l -> + if compare ex_id id = 0 then (meta, subindex) else assoc_tup3 id l + in + (match assoc_tup3 id l with + | _ , Some subindex -> Subindex.to_meta subindex + | Some meta, _ -> meta + | None, _ -> raise Not_found) in aux t @@ -786,11 +906,11 @@ module Exercise = struct (id, {g with contents = map_exercises f g.contents})) gs) | Exercises l -> - Exercises - (List.map (function - | (id, Some ex) -> (id, Some (f id ex)) - | x -> x) - l) + Exercises + (List.map (function + | (id, Some ex, Some subindex) -> (id, Some (f id ex), Some subindex) + | x -> x) + l) let rec mapk_exercises f t k = let rec mapk_list acc f l k = match l with @@ -806,8 +926,8 @@ module Exercise = struct @@ fun gs -> Groups gs |> k | Exercises l -> mapk_list [] (fun e k -> match e with - | (id, Some ex) -> - f id ex @@ fun ex -> (id, Some ex) |> k + |(id, Some ex, Some subindex) -> + f id ex @@ fun ex -> (id, Some ex, Some subindex) |> k | x -> x |> k) l @@ fun l -> Exercises l |> k @@ -819,7 +939,7 @@ module Exercise = struct acc gs | Exercises l -> List.fold_left (fun acc -> function - | (id, Some ex) -> f acc id ex + | (id, Some ex, Some _) -> f acc id ex | _ -> acc) acc l @@ -838,11 +958,11 @@ module Exercise = struct aux [] gs | Exercises l -> let rec aux acc = function - | (id, Some ex) :: r -> + | (id, Some ex, subindex) :: r -> (f id ex @@ function - | true -> aux ((id, Some ex) :: acc) r + | true -> aux ((id, Some ex, subindex) :: acc) r | false -> aux acc r) - | (_, None) :: r -> aux acc r + | (_, None, _) :: r -> aux acc r | [] -> k (Exercises (List.rev acc)) in aux [] l diff --git a/src/state/learnocaml_data.mli b/src/state/learnocaml_data.mli index 2408c5eec..4abc08c2c 100644 --- a/src/state/learnocaml_data.mli +++ b/src/state/learnocaml_data.mli @@ -245,10 +245,49 @@ module Exercise: sig end + module Subindex : sig + + type meta = Meta.t + + type part = { + subtitle : string; + subexercise : string; + student_hidden : bool; + student_weight : int; + teacher_weight : int; + } + + type t = { + meta : meta; + check_all_against : string option; + parts : part list; + } + + val to_meta : t -> meta + + val to_check : t -> string option + + val to_part : t -> part list + + val get_part_field : part -> string * string * bool * int * int + + val to_subindex : meta -> string option -> part list -> t + + val enc : t Json_encoding.encoding + + val find : t -> string -> part + + val find_opt : t -> string -> part option + + val map_exercises : + (('a * t option) list -> part list -> part list) -> + ('a * t option) list -> ('a * t option) list + end + module Index: sig type t = - | Exercises of (id * Meta.t option) list + | Exercises of (id * Meta.t option * Subindex.t option) list | Groups of (string * group) list and group = { title : string; diff --git a/src/state/learnocaml_store.ml b/src/state/learnocaml_store.ml index 20ca030e1..f66d94948 100644 --- a/src/state/learnocaml_store.ml +++ b/src/state/learnocaml_store.ml @@ -254,6 +254,12 @@ module Exercise = struct end + module Subindex = struct + include Exercise.Subindex + + (*let get_from_subindex subindex = + Exercise.Subindex*) + end module Index = struct include Exercise.Index @@ -279,6 +285,7 @@ module Exercise = struct with type id := id and module Meta := Meta and module Status := Status + and module Subindex := Subindex and module Index := Index) let get id = diff --git a/src/utils/learnocaml_partition_create.ml b/src/utils/learnocaml_partition_create.ml index 8358c4922..b032d9cb1 100644 --- a/src/utils/learnocaml_partition_create.ml +++ b/src/utils/learnocaml_partition_create.ml @@ -93,10 +93,10 @@ let asak_partition prof fun_name sol by_grade = let partition exo_name fun_name prof = Learnocaml_store.Exercise.get exo_name >>= fun exo -> - let prelude = Learnocaml_exercise.(access File.prelude exo) in - let prepare = Learnocaml_exercise.(decipher File.prepare exo) in + let prelude = Learnocaml_exercise.(access false File.prelude exo) in + let prepare = Learnocaml_exercise.(decipher false File.prepare exo) in let prelude = prelude ^ "\n" ^ prepare in - let solution = Learnocaml_exercise.(decipher File.solution exo) in + let solution = Learnocaml_exercise.(decipher false File.solution exo) in let solution = prelude ^ "\n" ^ solution in get_all_saves exo_name prelude >|= fun saves -> @@ -104,3 +104,4 @@ let partition exo_name fun_name prof = let by_grade = partition_by_grade fun_name lst in let bad_type,partition_by_grade = asak_partition prof fun_name solution by_grade in {not_graded; bad_type; partition_by_grade} + diff --git a/static/css/learnocaml_description.css b/static/css/learnocaml_description.css index 2f7260b5c..e34950c1e 100644 --- a/static/css/learnocaml_description.css +++ b/static/css/learnocaml_description.css @@ -43,6 +43,7 @@ body { display: block; font-weight: normal; position: relative; + border-bottom: thin solid #eee; } #learnocaml-exo-tabs > * > h1:first-child { margin-top: 5px; @@ -52,7 +53,7 @@ body { overflow: auto; flex: 1 3 auto ; } -#learnocaml-exo-tab-meta > h1::after { +#learnocaml-exo-tab-meta > * > h1::after { position: absolute; left: 0px; bottom: -5px; width: 100%; content:""; @@ -60,6 +61,89 @@ body { background: linear-gradient(to bottom, rgba(0,0,0,0.3) 0, transparent 100%) } +/* ----------------------- iframe --------------------------------*/ + +#learnocaml-exo-tab-text-iframe { + position : absolute; + right : 0px; + left : 0px; + bottom : 0px; + display : flex ; + flex-direction : column; +} + +#learnocaml-exo-tab-text-iframe > iframe { + height : 100%; +} +/* -------------------- Prelude -------------------------------- */ + +#learnocaml-exo-tab-text-prelude { + overflow: hidden; + left: 0; + top: 61px; + z-index: 1002; + border-bottom: thin solid #eee; + width: 100%; +} +#learnocaml-exo-tab-text-prelude > h1 { + position: relative; + flex: 0 0 auto; + background: #222; + color: #eee; + font-size: 20px; + line-height: 22px; + margin: 0; + padding-top: 10px; + padding-bottom: 10px; + display: block; + font-weight: normal; + z-index: 1005; + width: 100%; + text-align: center; +} +#learnocaml-exo-tab-text-prelude > h1:first-child { + margin-top: 0; +} +#learnocaml-exo-tab-text-prelude > h1 > button { + float: right; + border: none; + border-left: 1px #eee solid; + color: #eee; + padding-top: 10px; + padding-bottom: 10px; + margin-top: -10px; + margin-bottom: -10px; + font-size: 20px; + line-height: 22px; + z-index: 1006; + width: 20%; + background: #222; +} +#learnocaml-exo-tab-text-prelude > iframe { + border: none; + overflow: auto; + flex: 1 3 auto ; +} +#learnocaml-exo-tab-text-prelude > pre.toplevel-code { + flex: 0 1 auto; + height: 150px; + max-height: 150px; + background: #666; + margin: 0; + overflow: auto; + width: 100%; + position: absolute; +} +#learnocaml-exo-tab-text-prelude > h1::after { + position: absolute; + left: 0px; bottom: -5px; width: 100%; + content:""; + height:5px; background: pink; + background: linear-gradient(to bottom, rgba(0,0,0,0.3) 0, transparent 100%) +} + + + /* BEGIN excerpt from learnocaml_exercise.css */ .learnocaml-exo-meta-category ~ .exercise + .exercise { diff --git a/static/css/learnocaml_exercise.css b/static/css/learnocaml_exercise.css index 1ebc2c2da..a076da639 100644 --- a/static/css/learnocaml_exercise.css +++ b/static/css/learnocaml_exercise.css @@ -69,7 +69,33 @@ body { top: 61px; } } +/* ------------------------- Navigation ----------------------------*/ +#learnocaml-exo-tab-navigation{ + position: absolute; + z-index: 3; + display: flex; + flex-direction: row ; + border-bottom: 1px black solid; + background: #ddd; + padding: 0px; + flex: 0 0 auto; + width: 100%; + height:60px; +} +#learnocaml-exo-tab-navigation > button { + width: 20%; +} +#learnocaml-exo-tab-navigation > h4 { + width: 60%; + text-align:center; +} + /* -------------------- tabs and tab buttons ---------------------- */ +#learnocaml-exo-tab { + top: 60px; + position: absolute; + height: 100px; +} #learnocaml-exo-tab-buttons { position: absolute; z-index: 999; @@ -126,8 +152,22 @@ body { #learnocaml-exo-button-editor { display: none; } + #learnocaml-exo-tab { + left: 800px; right: 0px; top: 0px; + } + #learnocaml-exo-tab-navigation{ + height: 60px; + width: 100%; + border-bottom: 1px black solid; + position: absolute; + z-index: 3; + display: flex; + flex-direction: row; + background: #ddd; + padding: 0px; + } #learnocaml-exo-tab-buttons { - left: 800px; right: 0px; top: 0px; + left: 0px; right: 0px; top: 61px; } #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-toplevel { border-bottom: none; @@ -141,7 +181,7 @@ body { opacity: 1; } #learnocaml-exo-tabs > * { - left: 800px; top: 40px; right: 0px; bottom: 0px; + left: 800px; top: 101px; right: 0px; bottom: 0px; } #learnocaml-exo-tabs > #learnocaml-exo-tab-editor { width: 800px; left: 0; bottom: 0; top: 61px; @@ -154,6 +194,9 @@ body { #learnocaml-exo-toolbar { right: 0px; } + #learnocaml-exo-tab{ + width: 100%; + } #learnocaml-exo-tab-buttons { left: 0; right: 0px; } @@ -175,19 +218,29 @@ body { } @media (min-width: 550px) and (max-width: 1199px) { #learnocaml-exo-tab-buttons { - top: 60px; + top: 61px; } #learnocaml-exo-tabs > * { - top: 100px; + top: 161px; } } @media (max-width: 549px) { - #learnocaml-exo-tab-buttons { - top: 40px; - } - #learnocaml-exo-tabs > * { - top: 80px; - } + #learnocaml-exo-tab { + top:40px; + height:80px; + } + #learnocaml-exo-tab-navigation { + height:40px; + } + #learnocaml-exo-tab-navigation > h4 { + align-self:center; + } + #learnocaml-exo-tab-buttons { + top: 41px; + } + #learnocaml-exo-tabs > * { + top: 120px; + } } /* -------------------- Prelude -------------------------------- */ diff --git a/static/description.html b/static/description.html index bc56aab26..ac31728ce 100644 --- a/static/description.html +++ b/static/description.html @@ -34,7 +34,12 @@
-
+
+
+
+ +
diff --git a/static/exercise.html b/static/exercise.html index 87d1dd8d8..1873442c6 100644 --- a/static/exercise.html +++ b/static/exercise.html @@ -55,18 +55,30 @@
-
- - - - - - - - +
+
+ + +
+
+ + + + + + + + +