Skip to content

Commit

Permalink
Merge pull request #33 from jverzani/scorecard
Browse files Browse the repository at this point in the history
add scorecard type
  • Loading branch information
jverzani committed Sep 14, 2022
2 parents 4058569 + 3a09d10 commit 5aee3ad
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "QuizQuestions"
uuid = "612c44de-1021-4a21-84fb-7261cf5eb2d4"
authors = ["jverzani <jverzani@gmail.com> and contributors"]
version = "0.3.15"
version = "0.3.16"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Expand Down
6 changes: 3 additions & 3 deletions examples/documenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ radioq(choices, answer; hint="Which is the Greek symbol?") # hide


```@example quiz
choices = [raw"``\sin(\pi/6)``", raw"``\cos(\pi/4)``", raw"\tan(\pi/3)``"]
matches = [raw"``1/2``", raw"``\sqrt{2}/2``", raw"``\sqrt{3}/2``", raw"``\sqrt{3}``"]
choices = [raw"``\sin(\pi/6)``", raw"``\cos(\pi/4)``", raw"``\tan(\pi/3)``"]
matches = ["`1/2`", raw"`sqrt(2)/2`", raw"`sqrt(3)/2`", raw"`sqrt(3)`"]
answer = (1, 2, 4)
matchq(choices, matches, answer, label="Match the expression with the value")
```
Expand All @@ -71,5 +71,5 @@ l = @layout [a b; c d]
p = plot(p1, p2, p3, p4, layout=l)
imgfile = tempname() * ".png"
savefig(p, imgfile)
hotspotq(imgfile, (0,0), (1/2, 1/2), label="What best matches the graph of ``f(x) = -x^4``?")
hotspotq(imgfile, (0,1/2), (0, 1/2), label="What best matches the graph of ``f(x) = -x^4``?")
```
57 changes: 57 additions & 0 deletions examples/nytimes-quiz-9-13-2022.jmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# A NY Times quiz

```julia; echo=false
using QuizQuestions
```


The New York Times has interactive quizzes that have a `buttonq` type widget. In this demo we use that widget and the `scorecard` widget to give a summary when
the quiz is completed.

This is part of a weekly news quiz from September 13, 2022

1) Queen Elizabeth II, who died on Thursday, was Britain’s longest-serving monarch. When did she ascend to the throne?

```julia, echo=false
buttonq(string.(1932:20:2012), 2; explanation="""
Elizabeth ascended to the throne on Feb. 6, 1952, at age 25, upon the death of her father, King George VI. She reigned for the next seven decades. Read her [obituary](https://www.nytimes.com/2022/09/08/world/europe/queen-elizabeth-dead.html) and see her life in photos.
""")
```

2) A federal judge this week ordered the appointment of an independent arbiter to review documents that the F.B.I. seized from Donald Trump’s Mar-a-Lago club and residence. What is the arbiter known as?

```julia; echo=false
choices = ("A chief justice","A master chief","A special advocate",
"A special master", "A staff secretary")
explanation = """
Judge Aileen Cannon granted Trump’s request for a special master. She also temporarily barred the Justice Department from using the seized documents for any “investigative purpose.” The department on Thursday asked her to revisit her [decision](https://www.nytimes.com/2022/09/08/us/politics/trump-special-master-doj.html).
"""
buttonq(choices, 4; explanation=explanation)
```

3) Ten people were killed in knife attacks in Canada this week. Near where did the attacks occur?

```julia; echo=false
choices = (
"Banff National Park in Alberta",
"The James Smith Cree Nation reserve in Saskatchewan",
"Kejimkujik National Park in Nova Scotia",
"The Nipissing First Nation in Ontario",
"Stanley Park in British Columbia"
)
explanation = """
The victims were attacked in several locations across the James Smith Cree Nation and the village of Weldon in Saskatchewan. The attacks, allegedly committed by two brothers, stunned Canada and horrified the reserve’s tight-knit community.
"""
buttonq(choices, 2; explanation=explanation)
```


```julia; echo=false
scorecard([(0, 100) => """
## Congratulations

You got {{:correct}} correct of {{:total_questions}} questions
"""],
oncompletion=true;
not_completed_msg="Do all the questions")
```
3 changes: 2 additions & 1 deletion src/QuizQuestions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export numericq,
buttonq, radioq, booleanq, yesnoq,
multiq, multibuttonq, matchq,
stringq, fillblankq,
hotspotq, plotlylightq
hotspotq, plotlylightq,
scorecard

end
98 changes: 96 additions & 2 deletions src/html_templates.jl
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
## Could tidy up this HTML to make it look nicer
html_templates = Dict()

# code to support scorecard widget
scorecard_partial = """
quizquestions_scorecard["{{:ID}}"]["attempts"] += 1;
"""
scorecard_correct_partial = """
quizquestions_scorecard["{{:ID}}"]["correct"] = true;
if (typeof score_summary !== 'undefined') {
score_summary()
}
"""
scorecard_incorrect_partial = """
quizquestions_scorecard["{{:ID}}"]["correct"] = false;
if (typeof score_summary !== 'undefined') {
score_summary()
}
"""

# thumbs up/down don't show in my editor
grading_partial = """
$(scorecard_partial)
if(correct) {
msgBox.innerHTML = "<div class='pluto-output admonition note alert alert-success'><span> 👍&nbsp; {{#:CORRECT}}{{{:CORRECT}}}{{/:CORRECT}}{{^:CORRECT}}Correct{{/:CORRECT}} </span></div>";
var explanation = document.getElementById("explanation_{{:ID}}")
if (explanation != null) {
explanation.style.display = "none";
}
$(scorecard_correct_partial)
} else {
msgBox.innerHTML = "<div class='pluto-output admonition alert alert-danger'><span>👎&nbsp; {{#:INCORRECT}}{{{:INCORRECT}}}{{/:INCORRECT}}{{^:INCORRECT}}Incorrect{{/:INCORRECT}} </span></div>";
var explanation = document.getElementById("explanation_{{:ID}}")
if (explanation != null) {
explanation.style.display = "block";
}
$(scorecard_incorrect_partial)
}
"""

Expand All @@ -23,6 +45,15 @@ grading_partial = """
## Hint is put with label when present; otherwise, it appears at bottom of form.
## this is overridden with input widget in how show method is called
html_templates["question_tpl"] = mt"""
<script>
if (typeof quizquestions_scorecard === 'undefined') {
quizquestions_scorecard = {};
}
var ID = "{{:ID}}"
if (typeof quizquestions_scorecard[ID] === 'undefined') {
quizquestions_scorecard[ID] = {attempts: 0, correct: false};
}
</script>
<form class="mx-2 my-3 mw-100" name='WeaveQuestion' data-id='{{:ID}}' data-controltype='{{:TYPE}}'>
<div class='form-group {{:STATUS}}'>
<div class='controls'>
Expand Down Expand Up @@ -99,7 +130,7 @@ rb.addEventListener("change", function() {
})});
"""
## ----
html_templates["Buttonq"] = mt"""
html_templates["Buttonq"] = jmt"""
<div id="buttongroup_{{:ID}}" class="btn-group-vertical w-100">
{{#:BUTTONS}}
<button type="button" class="btn toggle-btn px-4 my-1 btn-light btn-block active" aria-pressed="false" id="button_{{:ID}}_{{:i}}" value="{{:ANSWER}}" style="width:100%;text-align:left; padding-left:10px; {{#:BLUE}}background:{{{:BLUE}}}{{/:BLUE}}" onclick="return false;">
Expand All @@ -112,15 +143,19 @@ document.querySelectorAll('[id^="button_{{:ID}}_"]').forEach(function(btn) {
btn.addEventListener("click", function(btn) {
var correct = this.value == "correct";
var id = this.id;
$(scorecard_partial)
if (!correct) {
$(scorecard_incorrect_partial)
{{#:GREEN}}this.style.background = "{{{:GREEN}}}";{{/:GREEN}}
var text = this.innerHTML;
this.innerHTML = "<em>{{{:INCORRECT}}</em>&nbsp;" + text ;
var explanation = document.getElementById("explanation_{{:ID}}")
if (explanation != null) {
explanation.style.display = "block";
}
}
} else {
$(scorecard_correct_partial)
}
document.querySelectorAll('[id^="button_{{:ID}}_"]').forEach(function(btn) {
btn.disabled = true;
btn.setAttribute("aria-pressed", "true");
Expand Down Expand Up @@ -342,3 +377,62 @@ document.getElementById("{{{:ID}}}").on("plotly_click", function(e) {
})
"""


## ------
## hacky way to keep a scorecard
html_templates["scorecard_tpl"] = """
<div id="scorecard"></div>
<script>
function score_summary() {
var s = quizquestions_scorecard;
var score = []; // array of arrays
var n = 0;
var n_correct = 0;
var n_attempted = 0;
var n_attempts = 0;
Object.entries(s).forEach(([key, value]) => {
n++;
if (value["correct"]) {
correct = 1;
n_correct++
} else {
correct = 0
};
attempts = value["attempts"];
if (attempts == 0) {
attempted = 0
} else {
attempted = 1;
n_attempted++
};
n_attempts = n_attempts + attempts;
score.push([correct, attempts, attempted]);
})
var completed = (n_attempted == n);
{{#:ONCOMPLETION}}if (completed) { {{/:ONCOMPLETION}}
var percent_correct = (n_correct / n) * 100
{{{:MESSAGE}}}
txt = txt.replace("{{:attempted}}", n_attempted);
txt = txt.replace("{{:total_attempts}}", n_attempts) ;
txt = txt.replace("{{:correct}}", n_correct);
txt = txt.replace("{{:total_questions}}", n);
{{#:ONCOMPLETION}}
} else {
// not completed
txt = "{{:NOT_COMPLETED_MSG}}";
}
{{/:ONCOMPLETION}}
el = document.getElementById("scorecard")
if (el !== null && txt.length > 0) {
el.innerHTML = txt;
}
}
</script>
"""
57 changes: 57 additions & 0 deletions src/question_types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -600,3 +600,60 @@ function plotlylightq(p, xs=(-Inf, Inf), ys=(-Inf,Inf);
correct_answer=nothing)
PlotlyLightQ(p, xs, ys, label, hint, explanation, correct_answer)
end

## -----

struct Scorecard <: Question
values
ONCOMPLETION::Bool
NOT_COMPLETED_MSG
end

"""
scorecard(values; oncompletion::Bool=false, not_completed_msg::String="")
Add a scorecard
* `values` is a collection of pairs, `interval => message`. See below.
* The `oncompletion` flag can be set to only show the message if all the questions have been attempted. When set, and not all questions have been attempted, the `not_completed_msg` message is shown.
The interval is specified as `(l,r)`, with `0 <= l < r <= 100`, or *optionally* as `(l,r,interval_type)` where `interval_type`, a string, is one of `"[]"`, `"[)"`,`"(]"`, or `"()"`. The default interval type is `"[)"` unless `r` is `100`, in which case it is `"[]"`.
The message is shown when the percent correct is in the interval. The following values are substituted, when present:
- `{{:total_questions}}` - the number of total questions
- `{{:correct}}` - the number of correct answers
- `{{:attempted}}` - the number of attempted questions
- `{{:total_attempts}}` -- the number of attempts.
The message may have Markdown formatting.
Example
```
values = [(0,99)=>"Keep trying",
(99, 100) => "You got {{:correct}} **correct** of {{:total_questions}} total *questions*"]
scorecard(values)
```
"""
function scorecard(values;
oncompletion::Bool=false,
not_completed_msg::String = "")

function _tohtml(txt)
txt = _markdown_to_html(txt)
txt = replace(txt, "&#123;" => "{") # replace
txt = replace(txt, "&#125;" => "}")
txt = chomp(txt)
txt
end

NOT_COMPLETED_MSG = oncompletion ? not_completed_msg : ""
Scorecard(
[first(pair) => _tohtml(pair[end]) for pair values],
oncompletion,
chomp(_markdown_to_html(NOT_COMPLETED_MSG))
)

end
37 changes: 37 additions & 0 deletions src/show_methods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,40 @@ function prepare_question(x::PlotlyLightQ, ID)

(FORM, GRADING_SCRIPT)
end


## ---
function Base.show(io::IO, m::MIME"text/html", x::Scorecard)
tpl = html_templates["scorecard_tpl"]

## make javascript conditions
msg = IOBuffer()

for (i, pr) enumerate(x.values)
I, txt = pr
if length(I) == 2
l,r = I
lbrace, rbrace = (">=", ifelse(r==100, "<=", "<"))
else
l,r,braces = I
braces ("[]", "[)", "(]", "()") || throw(ArgumentError("""brace specification is incorrect. Use one of "[]", "[)", "(]", "()" """))
lbrace = ifelse(braces[1:1] == "[", ">=", ">")
rbrace = ifelse(braces[2:2] == "]", "<=", "<")
end
txt = replace(txt, "\"" => "")
println(msg, "if (percent_correct $lbrace $l && percent_correct $rbrace $r) {",)
println(msg, """txt = `\n$txt\n`;""") # use `` for javascript multiline string
print(msg, "}")
print(msg, ifelse(i < length(x.values), " else ", "\n"))
end

Mustache.render(io, tpl;
MESSAGE=String(take!(msg)),
ONCOMPLETION=x.ONCOMPLETION,
NOT_COMPLETED_MSG=x.NOT_COMPLETED_MSG,
attempted = "{{:attempted}}", # hack
total_attempts = "{{:total_attempts}}",
correct = "{{:correct}}",
total_questions = "{{:total_questions}}"
)
end
10 changes: 10 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,14 @@ using Test
r = plotlylightq("empty"; label="XXX")
@test r.label == "XXX"

r = scorecard([(0,60) => "not passing",
(60,70) => "D range",
(70, 80) => "C range",
(80, 90) => "B range",
(90, 100) => """
# Congratulations
You will get an **A**. You got {{:correct}}/{{:total_questions}}.
"""])
@test r.ONCOMPLETION == false # default setting

end

2 comments on commit 5aee3ad

@jverzani
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/68276

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.3.16 -m "<description of version>" 5aee3ada54f78ee1323e5bef061205e0c807941f
git push origin v0.3.16

Please sign in to comment.