Commit 71d53bb0 authored by Bruno Burke's avatar Bruno Burke 🍔
Browse files

Merge branch 'master' into '7-prepare-function-for-simple-text-exercise'

# Conflicts:
#   src/clj/lernmeister/components/css.clj
#   src/devcards/lernmeister/components/sample_data.cljs
parents eadb0125 2048cdac
Pipeline #2394 passed with stages
in 2 minutes and 29 seconds
/*.log
/*-init.clj
/resources/public/js
/resources/public/css
/resources/public/node_modules
out
pom.xml
pom.xml.asc
......
# Add new Exercise-Types
For adding a new exercise-type you create a directory inside ``/src/cljc/lernmeister/components/exercise_types/`` named after your exercise-type and in there a core.cljc file.
Exercise-Types are mostly platform-independent, so prefer to implement it in cljc-files.
Some parts are frontend-only (**renderer** and **edit**) and can therefore be implemented in cljs.
A new Exercise-Type defines an export-map which consists of the following keys/values:
* **:type**
* A unique keyword for this exercise-type, e.g. :multiple-choice
* **:title**
* The title of this exercise-type. This value will be shown to the user in the Dropdown element of the exercise-editor.
* **:edit**
* A map of two functions/components for rendering the edit/new exercise dialog:
* settings:
* A reagent-component which renders the settings part. This part contains settings like *shuffled* or *input-format*.
* additional-forms:
* A reagent-component which renders additional forms like answer options.
* ```clojure
{:settings your-settings-component
:additional-forms your-additional-forms-component}
```
* **:renderer**
* A reagent-component which renders the exercise.
* This component should allow one parameter, the exercise.
* By default this component will be used with three keywords (which can be nil):
* *result* - the result map, which is generated by the exercise-checker
* *answer* - the answer from the user
* *on-change* - a function which is called if something is changed/clicked in the exercise. Normally this sets/changes the answer.
* ``[exercise & {:keys [result answer on-change]}]``
* **:checker**
* A function to check an answer to this exercise.
* Parameters:
* exercise - the exercise itself
* answer - an answer
* callback - a function which is called after checking the answer
* this function returns a result object
* **:spec**
* A spec which evaluates new exercises. Check [Clojure.spec](https://clojure.org/guides/spec) for informations or just look at existing exercise-types.
To activate your exercise-type in the lernmeister.components library, it has to be registered in ``/src/cljc/lernmeister/components/core.cljc``.
(defproject lernmeister.components "0.1.9"
(defproject lernmeister.components "0.1.11"
:dependencies [[org.clojure/clojure "1.9.0-beta2"]
[org.clojure/clojurescript "1.9.946"]
[reagent "0.7.0"]
[garden "1.3.3"]
[hickory "0.7.1"]
[cljsjs/katex "0.7.1-0"]
[clj-http "3.7.0"]
[cljs-ajax "0.7.3"]
[cheshire "5.8.0"] ;;; clj-http json support
[devcards "0.2.4" :exclusions [cljsjs/react]]]
:min-lein-version "2.5.3"
:source-paths ["src/cljc" "src/cljs" "src/clj"]
:test-paths ["test/clj" "test/cljc"]
:plugins [[lein-cljsbuild "1.1.7"]
[lein-garden "0.3.0"]]
[lein-garden "0.3.0"]
[lein-npm "0.6.2"]]
:npm {:root "resources/public"
:dependencies [[codemirror "^5.29.0"]
[materialize-css "^0.99.0"]
[quill "^1.3.2"]]}
:clean-targets ^{:protect false} ["resources/public/js"
"target"
......
......@@ -7,6 +7,10 @@
<link rel="stylesheet" href="css/screen.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.99.0/js/materialize.min.js"></script>
<script src="node_modules/codemirror/lib/codemirror.js"></script>
<script src="node_modules/codemirror/mode/python/python.js"></script>
<link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css">
<style id="com-rigsomelight-devcards-addons-css"></style>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
......
......@@ -2,5 +2,4 @@
(:require [garden.def :refer [defstyles]]))
(defstyles screen
[:div {:color "green"}]
[:.shifttotop {:margin-top "-1rem"}])
(ns lernmeister.components.exercise-types.programming.code-check
(:require [clj-http.client :as client]
[lernmeister.components.options :refer [get-option]]))
(defn get-code-runner-url []
(str (:code-runner-url (get-option :programming-exercise))))
(defn get-code-runner-api-key []
(:code-runner-api-key (get-option :programming-exercise)))
(defn run-code [code language]
(println "Send code: " code)
(:body (client/post (get-code-runner-url) {:form-params {:code code :language language}
:headers {"X-API-Key" (get-code-runner-api-key)}
:coerce :always
:as :json
:content-type :json})))
(defn is-correct? [result]
(when (and result (not (:isError result)) (:stdout result))
(not (-> (:stdout result)
clojure.string/trim
clojure.string/trim-newline
clojure.string/upper-case
(clojure.string/ends-with? "FALSE")))))
(defn unit-test [unit-test answer]
(if (is-correct? (run-code (str answer "\n" (:testcode unit-test)) "python2"))
{:id (:id unit-test) :correct true :points (:correct-points unit-test)}
{:id (:id unit-test) :correct false :points (:incorrect-points unit-test)}))
(defn check [exercise answer callback]
(callback)
(let [unit-tests (:unit-tests exercise)
results (map #(unit-test % answer) unit-tests)
test-frequencies (frequencies (map :correct results))
points (reduce + (map :points results))
points-max (reduce + (map #(apply max ((juxt :correct-points :incorrect-points) %)) unit-tests))]
{:unit-tests results
:passed (get test-frequencies true)
:failed (get test-frequencies false)
:points points
:points-max points-max}))
......@@ -7,7 +7,8 @@
:text answer
:integer answer
:number answer
:word answer))
:word answer
:vector answer))
(def prepare-answer (comp string/lower-case string/trim str))
......@@ -30,6 +31,28 @@
true
false))
(def input-format-checker
{:vector (fn [o e v2]
(let [v1 (:answer e)]
(and (every? number? v2)
(= (:dimensions o (count v1)) (count v1) (count v2))
(every? identity (map #(= 0.0 (double (- %1 %2))) v1 v2)))))
:number (fn [o e n2]
(let [n1 (:answer e)]
(and
(if (:unit o)
(= (:unit n1) (:unit n2))
true)
(if (:variable o)
(= (:variable n1) (:variable n2))
true)
(if-let [epsilon (:epsilon o 0)]
(<= (Math/abs (- (:value n1) (:value n2))) epsilon)
(= 0.0 (- (:value n1) (:value n2)))))))})
(defn get-input-format-checker [input-format options]
(partial (get input-format-checker input-format) options))
(defn expression-check [exercise answer callback]
(let [answer (get-expression-answer exercise answer)]
(callback)
......@@ -39,7 +62,8 @@
points-max (max correct-points incorrect-points)]
(if ((case (:type e)
:text text-expression-check
:regex regex-expression-check) e answer)
:regex regex-expression-check
:input-format (get-input-format-checker (:input-format exercise) (:input-format-options exercise))) e answer)
{:points correct-points :points-max points-max}
{:points incorrect-points :points-max points-max})))
(:expressions exercise))
......
......@@ -2,7 +2,7 @@
#?(:cljs (:require [cljs.spec.alpha :as s])
:clj (:require [clojure.spec.alpha :as s])))
(def input-formats {:word "Wort" :text "Text" :binary "Binärzahl" :number "Zahl" :integer "Ganzzahl"})
(def input-formats {:word "Wort" :text "Text" :binary "Binärzahl" :number "Zahl" :integer "Ganzzahl" :vector "Vektor"})
(s/def :expression/incorrect-points number?)
(s/def :expression/correct-points number?)
......
......@@ -8,27 +8,44 @@
(= a1 a2)
(= (prepare-answer a1) (prepare-answer a2))))
(defn unordered-list-check [exercise answer]
(let [answer (into #{} answer)
user-answers (map
(fn [a]
(if-let [correct-answer (first (filter #(compare-answers exercise a (:text %)) (:answers exercise)))]
{:text a :correct true :points (:points correct-answer)}
{:text a :correct false :points 0}))
answer)
other-answers (filter (comp not (partial answer) :text) (:answers exercise))
points (+ (reduce + (map :points user-answers))
(reduce + (map :missed-points other-answers)))]
{:answers user-answers
:points points
:points-max (reduce + (map #(apply max ((juxt :points :missed-points) %))
(:answers exercise)))}))
(defn ordered-list-check [exercise answer]
(let [exercise-answers (:answers exercise)
answers (map-indexed
(fn [idx a]
(if (compare-answers exercise a (:text (get exercise-answers idx)))
{:text a :correct true :points (:points (get exercise-answers idx) 0)}
{:text a :correct false :points (:missed-points (get exercise-answers idx) 0)}))
answer)
points (reduce + (map-indexed #(if (compare-answers exercise (get answer %1) (:text %2))
(:points %2)
(:missed-points %2)) exercise-answers))]
{:answers answers
:points points
:points-max (reduce + (map #(apply max ((juxt :points :missed-points) %))
exercise-answers))}))
(defn list-check [exercise answer callback]
(let [answer-options (into {} (map (juxt :id identity) (:answers exercise)))
correct-answers (:answers exercise)
correct-ids (into #{} (map :id correct-answers))]
(callback)
(let [answers
(if (:ordered exercise)
(map-indexed
(fn [idx a]
(if (compare-answers exercise a (:text (get correct-answers idx)))
{:text a :correct true :points (:points (get correct-answers idx))}
{:text a :correct false :points (:missed-points (get correct-answers idx))}))
answer)
(map
(fn [a]
(if-let [correct-answer (first (filter #(compare-answers exercise a (:text %)) (:answers exercise)))]
{:text a :correct true :points (:points correct-answer)}
{:text a :correct false :points 0}))
(into #{} answer)))
points (reduce + (map :points answers))]
{:answers answers
:points points
:points-max (reduce + (map #(apply max ((juxt :points :missed-points) %))
(:answers exercise)))})))
(if (:ordered exercise)
(ordered-list-check exercise answer)
(unordered-list-check exercise answer))))
(ns lernmeister.components.exercise-types.programming.check)
(ns lernmeister.components.exercise-types.programming.check
#?(:clj (:require [lernmeister.components.exercise-types.programming.code-check :refer [check]])))
(defn programming-check [exercise answer callback]
#?(:cljs (do (callback) {:points 0 :points-max 0})
#_(re-frame/dispatch [:test-code answer exercise callback])
:clj (do (callback) {:points 0 :points-max 0})))
:clj (check exercise answer callback)))
(ns lernmeister.components.options)
(defonce options (atom {}))
(defn add-option! [key value]
(println "Add Option " key)
(swap! options assoc key value))
(defn get-option [key]
(get @options key))
......@@ -18,15 +18,20 @@
[:span.light-green-text "✔ "])
)
(defn neutral-tick []
(fn []
[:span.blue-text "◌ "])
)
(defn wrong-tick []
(fn []
[:span.red-text "✘ "])
)
(defn binary-input [& {:keys [default-bits result on-change-fn]}]
(let [bits (reagent/atom (or default-bits (mapv (fn [] false) (range 8))))
(defn binary-input [& {:keys [default-bits result on-change-fn bitcount]}]
(let [bits (reagent/atom (or default-bits (vec (repeat (or bitcount 8) false))))
id (gensym)]
(fn [& {:keys [default-bits result on-change-fn]}]
(fn [& {:keys [default-bits result on-change-fn bitcount]}]
[:div.row {:style {:margin "auto auto auto auto" :display :inline-block}}
[:div.col.s12.center
[:p.flow-text.center
......@@ -39,7 +44,7 @@
[:div.col.s12
[:div.row
[:div.col.s12.center
(doall (for [i (range 8)]
(doall (for [i (range (count @bits))]
[:div {:key (str id "-cb-" i) :style {:display :inline-block}}
[:input {:id (str id "-cb-" i)
:type "checkbox"
......@@ -60,7 +65,15 @@
)
)
(defn codebox [code]
[:code {:style {:border "1px solid black"
:background-color "#EEEEFF"
:white-space :pre-line
:padding "10px"
:font-size "0.9em"
:display :inline-block}}
(str code)
])
(ns lernmeister.components.exercise-types.expression.views.edit
(:require [cljs.spec.alpha :as s]
[reagent.core :as reagent]
[lernmeister.components.exercise-types.expression.views.show :as show]
[lernmeister.components.material-design :as md])
(:use [lernmeister.components.jshelper :only [get-unique-id try-number-parse]]))
(:use [lernmeister.components.jshelper :only [get-unique-id try-number-parse]]
[lernmeister.components.helper :only [vec-remove]]))
(def input-formats {:word "Wort" :text "Text" :binary "Binärzahl" :number "Zahl" :integer "Ganzzahl"})
(def input-formats {:word "Wort"
:text "Text"
:binary "Binärzahl"
:number "Zahl"
:vector "Vektor"})
(defn get-new-expression []
{:id (get-unique-id "ae") :text "" :type :text :points 0})
{:id (get-unique-id "ae") :text "" :type :input-format :correct-points 0 :incorrect-points 0})
(def input-format-options
{:number (fn [expression]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options")
:type "text"
:value (or (:precision @expression) 0.00)
:on-change #(swap! expression assoc :precision (-> % .-target .-value))}]
[:label.active {:for (str (:id @expression) "-input-format-options")} "Maximale Abweichung"]])})
(defn case-sensitive-option [expression]
[:div.input-field
[:input {:id (str (:id @expression) "-case-sensitivity")
:type "checkbox"
:checked (:case-sensitive @expression)
:on-change #(swap! expression assoc :case-sensitive (-> % .-target .-checked))}]
[:label {:for (str (:id @expression) "-case-sensitivity")} "Groß-/kleinschreibung beachten"]])
(defn show-expression []
(fn [expression index]
[:strong "Ausdruck"]
(defn number-format-options [expression]
(fn [expression]
[:div.row
[:div.input-field.col.s12.m4
[md/select (reagent/cursor expression [:type]) {:text "Text" :regex "Reguläre Ausdrücke"} (str "expression-type-" index)]
[:label {:for (str "expression-type-" index)} "Typ"]]
[:div.input-field.col.s6.m4
[:input {:id (str "expression-points-correct-" index)
:type "text"
:value (:correct-points @expression)
:on-change #(swap! expression assoc :correct-points (-> % .-target .-value try-number-parse))}]
[:label.active {:for (str "expression-points-correct-" index)} "Punkte (korrekt)"]]
[:div.input-field.col.s6.m4
[:input {:id (str "expression-points-incorrect-" index)
:type "text"
:value (:incorrect-points @expression)
:on-change #(swap! expression assoc :incorrect-points (-> % .-target .-value try-number-parse))}]
[:label.active {:for (str "expression-points-incorrect-" index)} "Punkte (fehlerhaft)"]]
[:div.input-field.col.s12.m12
[:input {:id (str "correct-expression-" index)
:type "text"
:value (:text @expression)
:on-change #(swap! expression assoc :text (-> % .-target .-value))}]
[:label.active {:for (str "correct-expression-" index)} "Ausdruck"]]
[:div.col.s12.m6
[:fieldset
[:legend "Zusätzliche Felder"][:br]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options-variable")
:type "checkbox"
:checked (get-in @expression [:input-format-options :variable])
:on-change #(swap! expression update :input-format-options
merge {:variable (-> % .-target .-checked)})}]
[:label.active {:for (str (:id @expression) "-input-format-options-variable")} "Variable"]]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options-unit")
:type "checkbox"
:checked (get-in @expression [:input-format-options :unit])
:on-change #(swap! expression update :input-format-options
merge {:unit (-> % .-target .-checked)})}]
[:label.active {:for (str (:id @expression) "-input-format-options-unit")} "Einheit"]]
]]
[:div.col.s12.m6
[:fieldset
[:legend "Eigenschaften"][:br]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options-integer")
:type "checkbox"
:checked (get-in @expression [:input-format-options :integer])
:on-change #(swap! expression update :input-format-options
merge {:integer (-> % .-target .-checked)})}]
[:label.active {:for (str (:id @expression) "-input-format-options-integer")} "Ganze Zahlen"]]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options-epsilon")
:type "text"
:value (get-in @expression [:input-format-options :epsilon] 0)
:on-change #(swap! expression update :input-format-options
merge {:epsilon (.replace (-> % .-target .-value) "," ".")})}]
[:label.active {:for (str (:id @expression) "-input-format-options-epsilon")} "Erlaubte Abweichung"]
]]]
]))
(def input-format-options
{:number number-format-options
:word case-sensitive-option
:text case-sensitive-option
:vector (fn [expression]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options-dimension")
:type "text"
:value (get-in @expression [:input-format-options :dimensions] 0)
:on-change #(swap! expression update :input-format-options
merge {:dimensions (-> % .-target .-value str js/parseInt)})}]
[:label.active {:for (str (:id @expression) "-input-format-options-dimension")} "Dimensionen"]])
:binary (fn [expression]
[:div.input-field
[:input {:id (str (:id @expression) "-input-format-options-bits")
:type "text"
:value (get-in @expression [:input-format-options :bits] 0)
:on-change #(swap! expression update :input-format-options
merge {:bits (-> % .-target .-value str js/parseInt)})}]
[:label.active {:for (str (:id @expression) "-input-format-options-bits")} "Bits"]])})
(defn show-expression [expression index exercise & {:keys [delete-fn]}]
(fn [expression index exercise & {:keys [delete-fn]}]
[:div.card
[:div.row.card-content
[:p [:strong "Ausdruck " (inc index)]]
[:div.input-field.col.s12.m4
[md/select (reagent/cursor expression [:type])
{:text "Text" :regex "Reguläre Ausdrücke" :input-format "Eingabeformat"}
(str "expression-type-" index)]
[:label {:for (str "expression-type-" index)} "Typ"]]
[:div.input-field.col.s6.m4
[:input {:id (str "expression-points-correct-" index)
:type "text"
:value (:correct-points @expression)
:on-change #(swap! expression assoc :correct-points (-> % .-target .-value try-number-parse))}]
[:label.active {:for (str "expression-points-correct-" index)} "Punkte (korrekt)"]]
[:div.input-field.col.s6.m4
[:input {:id (str "expression-points-incorrect-" index)
:type "text"
:value (:incorrect-points @expression)
:on-change #(swap! expression assoc :incorrect-points (-> % .-target .-value try-number-parse))}]
[:label.active {:for (str "expression-points-incorrect-" index)} "Punkte (fehlerhaft)"]]
(if (= (:type @expression) :input-format)
[:div.input-field.col.s12
(if-let [renderer (show/get-renderer (:input-format exercise))]
[renderer exercise
:answer (:answer @expression)
:on-change-fn #(swap! expression assoc :answer %)]
[:span.red "Ungültiges Eingabeformat definiert."])
]
[:div.input-field.col.s12.m12
[:input {:id (str "correct-expression-" index)
:type "text"
:value (:text @expression)
:on-change #(swap! expression assoc :text (-> % .-target .-value))}]
[:label.active {:for (str "correct-expression-" index)} "Ausdruck"]])]
[:div.card-action
[:a.btn-flat {:on-click delete-fn} "🗙 Entfernen"]]
]))
(defn new-exercise-form []
(defn new-exercise-form [expressions exercise]
(reagent/create-class
{:display-name "expression-form"
:reagent-render
(fn [expressions]
(fn [expressions exercise]
[:div.new-question.card-panel.z-depth-3
[:p [:b "Expressions"]]
(map-indexed
(fn [index expression]
^{:key index}[show-expression (reagent/cursor expressions [index]) index]) @expressions)
^{:key index}[show-expression
(reagent/cursor expressions [index])
index exercise
:delete-fn #(swap! expressions vec-remove index)]) @expressions)
[:a.btn.blue
{:on-click
#(swap! expressions conj (get-new-expression))} "+"]])}))
......@@ -73,12 +155,12 @@
[:div.input-field.col.s12
[:div.row
[:div.col.s12 [:p [:b "Optionen:"]]]
[:div.input-field.col.s12.m4.l3
[:div.input-field.col.s12
[md/select (reagent/cursor exercise [:input-format]) input-formats "expression-input-format"]
[:label {:for "expression-input-format"} "Eingabeformat"]]
(when-let [options (get input-format-options (:input-format @exercise))]
[:div.col.s12.m4.l3
[:div.col.s12
[options exercise]])]]]))}))
(defn additional-forms [exercise]
[new-exercise-form (reagent/cursor exercise [:expressions])])
[new-exercise-form (reagent/cursor exercise [:expressions]) @exercise])
(ns lernmeister.components.exercise-types.expression.views.show
(:require [reagent.core :as reagent]
[lernmeister.components.content-elements.core :as content-manager]