Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add new question feature #1

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
53543f7
Add new question feature
Jun 18, 2022
9cc549c
(W1) Improvements to fill-in-the-blanks question
Jul 4, 2022
93f91ba
(W2) Fix misc errors, add in threshold and intermediate result option
Jul 4, 2022
ef8fad9
.min changes
Jul 4, 2022
f064a95
Revert several changes
Jul 6, 2022
e03adb3
Last reversion
Jul 6, 2022
e22ff93
Add in multiblanks question type
Jul 6, 2022
f6cacaf
Merge pull request #3 from weiquu/branch_multiblanks
weiquu Jul 6, 2022
b39e34a
Fix linting issues
Jul 6, 2022
bb356ca
Merge branch 'branch_question' of github.com:weiquu/markbind into bra…
Jul 6, 2022
c6d3ca4
UX changes
Jul 10, 2022
3846e5a
Attempt to add tests
Jul 12, 2022
1c8fb71
Move checking of answers to QOption
Jul 12, 2022
f20f157
Quick change
Jul 12, 2022
ca3b0cc
Update 1 test
Jul 13, 2022
1871f8c
Add remaining tests
Jul 13, 2022
c565659
Align first test to the rest
Jul 13, 2022
02c9045
Add 1 test and update 1 test for Quiz
Jul 13, 2022
55d2147
Update user guide with new question type
Jul 15, 2022
8c3f301
Make several code quality and CSS changes
Jul 18, 2022
6a751a8
Update blanks question tests
Jul 18, 2022
b8b8213
Update User Guide
Jul 18, 2022
c167681
Update to kebab-case
Jul 18, 2022
9364c7f
Fix small typo
Jul 18, 2022
b5bd72f
Update docs/userGuide/syntax/questions.md
weiquu Jul 18, 2022
a403fdf
Update docs/userGuide/syntax/questions.md
weiquu Jul 18, 2022
bc423c4
Update docs/userGuide/syntax/questions.md
weiquu Jul 18, 2022
b7bba22
Update packages/vue-components/src/questions/QOption.vue
weiquu Jul 18, 2022
30c9634
Update questions.md
weiquu Jul 18, 2022
1b7b959
Change hide-intermediate-result to no-intermediate-result
Jul 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions docs/userGuide/syntax/questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Question and quiz components provide an easy way to test readers on the relevant

#### Introduction

Question components (`<question>`) can be one of the following types: **MCQ**, **Checkbox** or **Text**.
Question components (`<question>`) can be one of the following types: **MCQ**, **Checkbox**, **Fill-in-the-Blanks**, or **Text**.

In all cases, content directly inserted in between `<question>...</question>` will be inserted into the **question body**.

Expand Down Expand Up @@ -135,7 +135,7 @@ Placing the question into the header is entirely optional. You may also wish to
****Options and Slots common to all question types****
Name | Type | Default | Description
--- | --- | --- | ---
type | `String` | `''` | The type of question. Supports `mcq`, `checkbox` or `text`.
type | `String` | `''` | The type of question. Supports `mcq`, `checkbox`, `blanks`, or `text`.
header{{slot_info_trigger}} | `String` | `''` | The markup to insert into the question header. The header is omitted if this is not provided.
hint{{slot_info_trigger}} | `String` | `''` | The content to display in the hint box.

Expand Down Expand Up @@ -217,6 +217,83 @@ correct | `Boolean` | `false` | Whether this option (placed under either a MCQ o
reason{{slot_info_trigger}} | `String` | `''` | The explanation markup to display for the option once the answer is checked.


#### Fill-in-the-Blanks Questions {.mt-4 .mb-3}

Fill-in-the-blanks questions are specified with the `type="blanks"` attribute.

Unlike MCQ and checkbox questions, answer checking is performed for each blank by providing keywords to check for in the user's answer through the `keywords` attribute in each `q-option`.
If no keywords are provided, the answer for that blank will always be marked as correct.

<box type="warning" seamless>

Keywords are validated by checking if the keyword matches the user's answer exactly (ignoring letter casing).
This works well for some
<popover header="When does validation work?">cases
<span slot="content">
When the keywords given are short and specific to the blank (eg. `abstraction`), it increases the chances that the blank will be validated correctly.
<br><br>
In contrast, something long and vague like `after discussing for a period of time` which can easily be expressed in a different way (eg. `after deliberating for a while`) would likely cause the blank to be validated incorrectly.
</span>
</popover>
and not others.

</box>

{% set blanksQuestion %}
<question type="blanks" hint="Google it!">

##### German sociologist __________ called the process of simultaneously analyzing the behavior of individuals and the society that shapes that behavior __________.

<q-option keywords="Norbert Elias, Elias" reason="That's his name!"></q-option>
<q-option keywords="figuration"></q-option>
</question>
{% endset %}

<include src="codeAndOutput.md" boilerplate>
<variable name="highlightStyle">html</variable>
<variable name="code">{{ blanksQuestion }}</variable>
</include>

By default, if the question has yet to be answered correctly, intermediate results will be shown beside each blank. You can specify a `no-intermediate-result` attribute to avoid this behvaiour (i.e. hide the result of each blank upon incorrect attempts).

<box type="tip" seamless>

Since the validation is imperfect, the minimum proportion of correct blanks needed for the entire question to be marked as correct can also be changed using the `threshold` attribute.

If you don't want to validate the answer at all, you may set the `threshold` attribute to `0`. Doing so always marks the entire question correct, and users will be able to see all intended answers.

</box>

{% set blanksQuestion2 %}
<question type="blanks" hint="What properties would you want these database transactions to have?" threshold=0.75 no-intermediate-result>

##### In computer science, ACID is a set of properties of database transactions intended to guarantee data validity despite errors, power failures, and other mishaps. These properties are: A for __________, C for __________, I for __________, and D for __________.

<q-option keywords="Atomicity, Atomic" reason="Meaning: either all occurs or nothing occurs"></q-option>
<q-option keywords="Consistency, Consistent"></q-option>
<q-option keywords="Isolation, Isolated"></q-option>
<q-option keywords="Durability, Durable"></q-option>
</question>
{% endset %}

<include src="codeAndOutput.md" boilerplate>
<variable name="highlightStyle">html</variable>
<variable name="code">{{ blanksQuestion2 }}</variable>
</include>

****Fill-in-the-Blanks Question specific Options and Slots****
Name | Type | Default | Description
--- | --- | --- | ---
threshold | `Number` | `0.5` | Minimum proportion of keywords that have to be matched in the user's answer for the answer to be marked as correct.
no-intermediate-result | `Boolean` | `False` | Hides the result of each blank after an incorrect attempt.

****`q-option` Options and Slots****
Name | Type | Default | Description
--- | --- | --- | ---
keywords | `String` | `''` | Comma delimited string of keywords or phrases to match the user's answer against.
reason{{slot_info_trigger}} | `String` | `''` | The explanation markup to display for the option once the answer is checked.


#### Text Questions {.mt-4 .mb-3}

Text questions are specified with the `type="text"` attribute.
Expand Down Expand Up @@ -292,13 +369,15 @@ Simply place the `<question>` components you want to include into the `<quiz>` c
<quiz>
<question type="mcq">...</question>
<question type="checkbox">...</question>
<question type="blanks">...</question>
<question type="text">...</question>
</quiz>
</variable>
<variable name="output" id="quiz-example">
<quiz>
{{ mcqQuestion }}
{{ checkboxQuestion }}
{{ blanksQuestion }}
{{ textQuestion }}
</quiz>
</variable>
Expand All @@ -320,6 +399,10 @@ intro | Slot | `Click start to begin` | Quiz intro markup. Overrides the `intro`
{{ mcqQuestion }}
```

```html { heading="Fill-in-the-Blanks questions" }
{{ blanksQuestion }}
```

```html { heading="Text questions" }
{{ textQuestion }}
```
Expand All @@ -328,6 +411,7 @@ intro | Slot | `Click start to begin` | Quiz intro markup. Overrides the `intro`
<quiz>
<question type="mcq">...</question>
<question type="checkbox">...</question>
<question type="blanks">...</question>
<question type="text">...</question>
</quiz>
```
Expand Down
209 changes: 208 additions & 1 deletion packages/vue-components/src/__tests__/Questions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('Mcq Questions and QOptions', () => {

// Click hint
await wrapper.find('button.btn-success').trigger('click');
// Click 'check' without having clicked any options
// Click 'check' with no input
await wrapper.find('button.btn-primary').trigger('click');

expect(wrapper.element).toMatchSnapshot();
Expand Down Expand Up @@ -274,6 +274,213 @@ describe('Checkbox Questions and QOptions', () => {
});
});

describe('Blank Questions and QOptions', () => {
test('of unanswered with shown hint and header renders correctly', async () => {
const option = {
render(h) {
return h(QOption, { props: { keywords: 'key' } });
},
};

const wrapper = mount(Question, {
propsData: { type: 'blanks' },
slots: {
default: [
'Question content',
option,
option,
option,
option,
],
header: 'unanswered blanks question header',
hint: 'blanks question hint',
},
provide: DEFAULT_INJECTIONS,
stubs: DEFAULT_STUBS,
});

// Click hint
await wrapper.find('button.btn-success').trigger('click');

expect(wrapper.element).toMatchSnapshot();
});

test('of answered correctly with unshown hint without header renders correctly', async () => {
const option = {
render(h) {
return h(QOption, { props: { keywords: 'key' } });
},
};

const wrapper = mount(Question, {
propsData: {
type: 'blanks',
threshold: 0.25,
},
slots: {
default: [
'Question content',
option,
option,
option,
option,
],
hint: 'blanks question hint',
},
provide: DEFAULT_INJECTIONS,
stubs: DEFAULT_STUBS,
});

// set correct input for blank 3
await wrapper.findAllComponents(QOption).at(2).find('input').setValue('key');
// set incorrect input for blank 4
await wrapper.findAllComponents(QOption).at(3).find('input').setValue('wrong');

// click 'check'
await wrapper.find('button.btn-primary').trigger('click');

// set input for blank 1 - should not change value since question answered
await wrapper.findAllComponents(QOption).at(0).find('input').setValue('key');

expect(wrapper.findAllComponents(QOption).at(0).vm.inputText).toMatch('');
expect(wrapper.element).toMatchSnapshot();
});

test('of checked wrongly with shown hint without header renders correctly', async () => {
const option = {
render(h) {
return h(QOption, { props: { keywords: 'key' } });
},
};

const wrapper = mount(Question, {
propsData: {
type: 'blanks',
threshold: 0.76,
noIntermediateResult: true,
},
slots: {
default: [
'Question content',
option,
option,
option,
option,
],
hint: 'blanks question hint',
},
provide: DEFAULT_INJECTIONS,
stubs: DEFAULT_STUBS,
});

// set correct input for blank 1
await wrapper.findAllComponents(QOption).at(0).find('input').setValue('key');
// set correct input for blank 2
await wrapper.findAllComponents(QOption).at(1).find('input').setValue('key');
// set correct input for blank 3
await wrapper.findAllComponents(QOption).at(2).find('input').setValue('key');

// click 'check'
await wrapper.find('button.btn-primary').trigger('click');
// click 'hint'
await wrapper.find('button.btn-success').trigger('click');
// set an incorrect input for blank 4
await wrapper.findAllComponents(QOption).at(3).find('input').setValue('wrong');
// click 'retry' -- no change
await wrapper.find('button.btn-primary').trigger('click');

expect(wrapper.findAllComponents(QOption).at(3).vm.inputText).toMatch('wrong');
expect(wrapper.element).toMatchSnapshot();
});

test('of checked wrongly with unshown hint with header with intermediate renders correctly', async () => {
const option = {
render(h) {
return h(QOption, { props: { keywords: 'key' } });
},
};

const wrapper = mount(Question, {
propsData: {
type: 'blanks',
threshold: 1,
},
slots: {
default: [
'Question content',
option,
option,
option,
option,
],
header: 'blanks question header',
hint: 'blanks question hint',
},
provide: DEFAULT_INJECTIONS,
stubs: DEFAULT_STUBS,
});

// set correct input for blank 1
await wrapper.findAllComponents(QOption).at(0).find('input').setValue('key');
// set correct input for blank 2
await wrapper.findAllComponents(QOption).at(1).find('input').setValue('key');
// set correct input for blank 3
await wrapper.findAllComponents(QOption).at(2).find('input').setValue('key');

// click 'check'
await wrapper.find('button.btn-primary').trigger('click');
// set an incorrect input for blank 4
await wrapper.findAllComponents(QOption).at(3).find('input').setValue('wrong');
// click 'retry' -- no change
await wrapper.find('button.btn-primary').trigger('click');

expect(wrapper.findAllComponents(QOption).at(3).vm.inputText).toMatch('wrong');
expect(wrapper.element).toMatchSnapshot();
});

test('of answered wrongly with header without hint renders correctly', async () => {
const option = {
render(h) {
return h(QOption, { props: { keywords: 'key' } });
},
};

const wrapper = mount(Question, {
propsData: {
type: 'blanks',
threshold: 0.5,
},
slots: {
default: [
'Question content',
option,
option,
option,
option,
],
header: 'blanks question header',
},
provide: DEFAULT_INJECTIONS,
stubs: DEFAULT_STUBS,
});

// set correct input for blank 2
await wrapper.findAllComponents(QOption).at(1).find('input').setValue('key');
// set incorrect input for blank 3
await wrapper.findAllComponents(QOption).at(2).find('input').setValue('wrong');
// set incorrect input for blank 4
await wrapper.findAllComponents(QOption).at(3).find('input').setValue('wrong');

// click 'check'
await wrapper.find('button.btn-primary').trigger('click');
// click 'show'
await wrapper.find('button.btn-info').trigger('click');

expect(wrapper.findAllComponents(QOption).at(0).vm.inputText).toMatch('');
expect(wrapper.element).toMatchSnapshot();
});
});

describe('Text Questions', () => {
test('of unanswered with shown hint, header without answer renders correctly ', async () => {
const wrapper = mount(Question, {
Expand Down
Loading