Commit 0627b685 authored by Bruno Burke's avatar Bruno Burke 🍔
Browse files

code-execution is working serverside and clientside

parent 1765e70e
Pipeline #2397 passed with stages
in 2 minutes and 27 seconds
/*.log
/*-init.clj
/resources/public/js
/resources/public/css
/resources/public/node_modules
out
pom.xml
pom.xml.asc
......
(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"]
......@@ -6,6 +6,7 @@
[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]]]
......@@ -14,7 +15,12 @@
: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"
......
......@@ -6,6 +6,10 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.99.0/css/materialize.min.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,4 +2,4 @@
(:require [garden.def :refer [defstyles]]))
(defstyles screen
[:div {:color "green"}])
)
(ns lernmeister.components.exercise-types.programming.code-check
(:require [clj-http.client :as client]))
(:require [clj-http.client :as client]
[lernmeister.components.options :refer [get-option]]))
(def code-runner (str "http://fb02tiitvm05:3001/compile"))
(def code-runner-api-key "...PyCodeExecution__3}dx")
(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 code-runner {:form-params {:code code :language language}
:headers {"X-API-Key" code-runner-api-key}
(: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})))
......
(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,6 +18,11 @@
[:span.light-green-text "✔ "])
)
(defn neutral-tick []
(fn []
[:span.blue-text "◌ "])
)
(defn wrong-tick []
(fn []
[:span.red-text "✘ "])
......@@ -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.programming.coderunner
(:require [lernmeister.components.options :refer [add-option! get-option]]))
(defn run-code [code handler & {:keys [code-runner-url timeoutMs]}]
(let [code-runner (:code-runner-fn (get-option :programming-exercise))]
(code-runner :params {:code code
:timeoutMs (or timeoutMs 750)}
:handler handler
:code-runner-url code-runner-url)))
......@@ -3,6 +3,7 @@
[lernmeister.components.content-elements.core :as content-manager]
[lernmeister.components.common :as common]
[lernmeister.components.content-elements.exercise :as exercises]
[lernmeister.components.exercise-types.programming.coderunner :refer [run-code]]
[clojure.string]))
(defn is-correct [result]
......@@ -49,72 +50,96 @@
))
))
(defn exercise-renderer [exercise & {:keys [result answer on-change]}]
(defn exercise-renderer [exercise & {:keys [result answer on-change status]}]
(let [eid (:id exercise)
cm-editor-id (str (gensym) "-" eid)
cm-editor (atom nil)
code-result (reagent/atom nil) ;(re-frame/subscribe [:code-result eid])
code-runner-pending (reagent/atom nil); (re-frame/subscribe [:code-runner-pending eid])
code-testrunner-pending (reagent/atom nil); (re-frame/subscribe [:code-testrunner-pending eid])
test-results (reagent/atom nil); (re-frame/subscribe [:code-test-results eid])
]
status (reagent/atom {})
run #(do
(swap! status assoc :code-runner-pending true)
(run-code (.getValue @cm-editor)
(fn [result]
(swap! status dissoc :code-runner-pending)
(swap! status assoc :code-result result))
:code-runner-url (:code-runner-url exercise)
))]
(reagent/create-class
{
:component-name "programming-exercise"
:component-did-mount
(fn []
(reset! cm-editor
(.fromTextArea js/CodeMirror (js/document.getElementById cm-editor-id)
(clj->js
{:mode "python"
:lineNumbers true})))
(.on @cm-editor "change" #(on-change (.getValue @cm-editor)))
(.setValue @cm-editor (or answer (:code-template exercise))))
(when-not result
(reset! cm-editor
(.fromTextArea js/CodeMirror (js/document.getElementById cm-editor-id)
(clj->js
{:mode "python"
:lineNumbers true})))
(.on @cm-editor "change" #(on-change (.getValue @cm-editor)))
(.setValue @cm-editor (or answer (:code-template exercise)))))
:reagent-render
(fn [exercise & {:keys [result answer on-change]}]
[:div.card
[:div.card-content
[:span.card-title [:b (:title exercise)]]
(doall (for [ce (:task-description exercise)]
^{:key (:id ce)}[(content-manager/get-renderer (:type ce)) ce]
))
[:textarea {:id cm-editor-id}]
(when @code-result
[:div
[:h2 "Auswertung"]
(if @code-runner-pending
[:div.progress.blue.lighten-5
[:div.indeterminate.blue]]
[:div.card.card-outline-success.mb-3.text-center
[:div.card-block
[:blockquote.card-blockquote
[:p @code-result]
]]
])
]
)
(when @test-results
[:div
[:h2 "Testresults"]
(if (pos? (count @code-testrunner-pending))
[:div.progress.blue.lighten-5
[:div.indeterminate.blue]]
(for [tr @test-results]
^{:key (str eid (first tr))}
[test-result (second tr) (first tr) (get-in exercise [:unit-tests (first tr)])]))
])]
[:div.card-action
(if @code-runner-pending
[:button.btn.green {:type "button" :disabled "disabled"} "Run"]
[:button.btn.light-green {:on-click identity #_(re-frame/dispatch [:run-code (.getValue @cm-editor) exercise])} "Run"]
)
" "
(if (pos? (count @code-testrunner-pending))
[:button.btn.orange {:type "button" :disabled "disabled"} "Test / Submit"]
[:button.btn.orange {:on-click identity #_(re-frame/dispatch [:test-code (.getValue @cm-editor) exercise])} "Test / Submit"]
)]
]
)})))
(let [test-results (get result :test-results)
coderesult (:code-result @status)
code-runner-pending (:code-runner-pending @status)
code-testrunner-pending (:code-testrunner-pending @status)]
[:div.card {:class
(cond
(and (:points result) (:points-max result)
(= (:points result) (:points-max result))) "green lighten-4"
(and (:points result) (:points-max result)
(< 0 (:points result) (:points-max result))) "yellow lighten-4"
(and (:points result) (<= (:points result) 0)) "red lighten-4"
:else "")}
[:div.card-content
[:span.card-title [:b (:title exercise)]]
(doall (for [ce (:task-description exercise)]
^{:key (:id ce)}[(content-manager/get-renderer (:type ce)) ce]))
(if-not result
[:textarea {:id cm-editor-id}]
[:div [:p "Deine Lösung:"]
[common/codebox answer]])
[:br]
(when coderesult
[:div
(if code-runner-pending
[:div.progress.blue.lighten-5
[:div.indeterminate.blue]]
(if (:isError coderesult)
[common/codebox (:stderr coderesult)]
[common/codebox (:stdout coderesult)]
))])
(when result
[:div.right.secondary-content
(let [passed (:passed result)
failed (:failed result)
tests (+ passed failed)]
(str passed "/" tests " erfolgreiche Unit-Tests "))
(cond
(and (:points result) (:points-max result)
(= (:points result) (:points-max result))) [common/correct-tick]
(and (:points result) (:points-max result)
(< 0 (:points result) (:points-max result))) [common/neutral-tick]
(and (:points result) (<= (:points result) 0)) [common/wrong-tick]
:else "")
(if (pos? (count code-testrunner-pending))
[:div.progress.blue.lighten-5
[:div.indeterminate.blue]]
(for [tr test-results]
^{:key (str eid (first tr))}
[test-result (second tr) (first tr) (get-in exercise [:unit-tests (first tr)])]))])]
(when-not result
[:div.card-action
(if code-runner-pending
[:button.btn.green {:type "button" :disabled "disabled"} "Run"]
[:button.btn.light-green {:on-click #(run)} "Run"]
)
" "
#_(if (pos? (count code-testrunner-pending))
[:button.btn.orange {:type "button" :disabled "disabled"} "Test / Submit"]
[:button.btn.orange {:on-click identity #_(re-frame/dispatch [:test-code (.getValue @cm-editor) exercise])} "Test / Submit"]
)])
])
)})))
......
......@@ -4,6 +4,7 @@
(:require
[lernmeister.components.first-card]
[devcards.lernmeister.components.tex-test-card]
[devcards.lernmeister.components.exercise-programming]
[devcards.lernmeister.components.exercise-editor-card]
[devcards.lernmeister.components.exercise-expression]
[devcards.lernmeister.components.exercise-sc-card]))
......
(ns devcards.lernmeister.components.exercise-programming
(:require-macros
[devcards.core :refer [defcard-doc
defcard-rg
defcard
mkdn-pprint-source]])
(:require
[lernmeister.components.sample-data :as data]
[lernmeister.components.core]
[lernmeister.components.options :refer [add-option!]]
[lernmeister.components.exercise-types.programming.views.show :as ex-show]
[lernmeister.components.content-elements.exercise :as exercise]
[lernmeister.components.exercise-types.core :refer [get-exercise get-types]]
[lernmeister.components.content-elements.core :as content-manager]
[lernmeister.components.material-design :as md]
[ajax.core :refer [GET POST]]
[clojure.pprint :refer [pprint]]
[devcards.core]
[reagent.core :as reagent]))
(add-option!
:programming-exercise {:code-runner-fn
(fn [& {:keys [params handler]}]
(POST "http://localhost:3005/compile"
{:handler handler
:params params
:format :json
:response-format :json
:keywords? true
:headers {:X-API-KEY "...PyCodeExecution__3}dx"}}))})
(defonce exercise-data data/programming-exercise)
(def answer (reagent/atom nil))
(defcard "# Programming Exercise-Type")
(defcard @exercise-data)
(defcard-rg prog-exercise
(fn []
[:div
[ex-show/exercise-renderer @exercise-data
;;{:points 3 :points-max 6 :correct true}
:answer @answer
:on-change #(reset! answer %)]]))
(defcard-rg prog-exercise-answered
(fn []
[:div
[ex-show/exercise-renderer @exercise-data
;;{:points 3 :points-max 6 :correct true}
:answer (:code-template @exercise-data)
:result {:failed 2 :passed 7 :points 2 :points-max 5}
:on-change #(reset! answer %)]]))
......@@ -112,3 +112,50 @@
"Eine Schnecke kriecht mit konstanter Geschwindigkeit einen Weinberg hinauf. Für die 150 m lange Strecke vom Sockel bis zum 90 m hohen Gipfel benötigt die Schnecke 270 s. Eine Steilkante von 75 m Höhe schließt sich direkt an den Gipfel an. Unten geht es noch 300 m abfällig weiter, bis das Höhenniveau des Bergsockels wieder erreicht ist. Die Schnecke beschließt, es ruhiger angehen zu lassen und schneckt die letzten 300 m mit 0,1 m/s weiter.\n1.\tWie lange war die Schnecke insgesamt unterwegs?",
:type "text"}],
:text ""}))
(def programming-exercise
(reagent/atom {:code-template "def manhattan(a, b):\n return 0",
:shuffled false,
:type :programming,
:unit-tests
[{:id "ut2017-06-27T09-38-52-G__919",
:code "",
:title "Reihenfolge der Parameter ist irrelevant",
:points 0,
:testcode
"import random\n\nn = 4\nv1 = random.sample(range(-100, 100), n)\nv2 = random.sample(range(-100, 100), n)\n\nprint(manhattan(v1,v2) == manhattan(v2,v1))",
:correct-points 1,
:incorrect-points 0}
{:id "ut2017-06-27T09-43-01-G__930",
:code "",
:title "Absolutwert wird berechnet",
:points 0,
:testcode
"print(manhattan([-2], [-4]) == manhattan([-4], [-2]) == 2)",
:correct-points 1,
:incorrect-points 0}
{:id "ut2017-06-27T09-45-56-G__933",
:code "",
:title "Korrekter Wert wird berechnet",
:points 0,
:testcode
"import random\n\ndef manhattan_distance(start, end):\n return sum(abs(e - s) for s,e in zip(start, end))\n\ncorrect = True\n \nfor i in range(0,100):\n n = random.randint(1,6)\n v1 = random.sample(range(-100, 100), n)\n v2 = random.sample(range(-100, 100), n)\n correct = correct and (manhattan_distance(v1,v2) == manhattan(v1,v2))\n \nprint(correct)",
:correct-points 1,
:incorrect-points 0}],
:title "Manhattan-Distanz",
:language :python3,
:id "ex2017-06-27T09-28-44-G__897",
:answers [],
:voting false,
:task-description
[{:id "ce2017-06-27T09-36-19-G__898",
:value
"Programmieren Sie eine Funktion mit dem Namen manhattan, welche die Manhattan-Distanz zwischen den Parametern a und b berechnet. \nBei a und b handelt es sich um beliebig-dimensionale Vektoren.",
:type "text"}
{:id "ce2017-06-29T13-43-34-G__42",
:value "",
:type "image",
:url
"data:image/png;base64,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"}],
:text ""}))
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment