Skip to content

Commit

Permalink
Arranged answers as learner selection first
Browse files Browse the repository at this point in the history
  • Loading branch information
rimutaka committed Oct 20, 2024
1 parent 51ac59d commit 260d912
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 70 deletions.
2 changes: 1 addition & 1 deletion rust/lambdas/question-handler/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub(crate) async fn my_handler(

// get the question from the DB and return as HTML with explanations
match question::get_exact(&topic, qid).await {
Ok(v) => json_response(Some(&v.format(QuestionFormat::HtmlFull)), 200),
Ok(v) => json_response(Some(&v.format(QuestionFormat::HtmlFull(Some(answers)))), 200),
Err(e) => text_response(Some(e.to_string()), 400),
}
}
Expand Down
59 changes: 45 additions & 14 deletions rust/types/src/question.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ use tracing::error;
/// Use the `FromStr` trait to convert the string to the enum.
pub enum QuestionFormat {
/// Return the full question in Markdown format for editing.
/// Header value: `markdown_full`.
MarkdownFull,
/// Return the full question in HTML format for rendering with explanations.
/// Header value: `html_full`.
HtmlFull,
/// Learner answers are enclosed in the Vec<u8>.
/// This is only valid in the context of a learner answering the question.
HtmlFull(Option<Vec<u8>>),
/// Return the short question in HTML format for the user to answer.
/// This is the default format if the header is absent or the value is none of the above.
HtmlShort,
}

Expand Down Expand Up @@ -45,6 +44,10 @@ pub struct Answer {
/// Only present if true.
#[serde(skip_serializing_if = "Option::is_none", default)]
c: Option<bool>,
/// Learner's choice. It is set to true if the learner selected this answer.
/// Present in JSON only if true.
#[serde(skip_serializing_if = "Option::is_none", default)]
sel: Option<bool>,
}

/// A question with multiple answers.
Expand Down Expand Up @@ -77,16 +80,16 @@ impl Question {
/// Converts markdown members (question, answers) to HTML.
/// Supports CommonMark only.
/// See https://crates.io/crates/pulldown-cmark for more information.
fn into_html(self) -> Self {
fn into_html(self, learner_answers: Option<Vec<u8>>) -> Self {
// the parser can have Options for extended MD support, but they don't seem to be needed

// convert the question to HTML
let parser = pulldown_cmark::Parser::new(&self.question);
let mut question = String::new();
push_html(&mut question, parser);
let mut question_as_html = String::new();
push_html(&mut question_as_html, parser);

// convert answers to HTML
let answers = self
let answers_as_html = self
.answers
.into_iter()
.map(|answer| {
Expand All @@ -101,13 +104,41 @@ impl Question {
e
});

Answer { a, e, c: answer.c }
Answer {
a,
e,
c: answer.c,
sel: None,
}
})
.collect();
.collect::<Vec<Answer>>();

// sort the answers so that the answered questions are at the top
let answers_as_html = match learner_answers {
Some(v) => {
// sort them into two buckets, then append unanswered to answered
// the original order in the buckets is preserved
let mut answered = Vec::with_capacity(self.correct as usize);
let mut unanswered = Vec::with_capacity(answers_as_html.len() - self.correct as usize);
for (idx, answer) in answers_as_html.into_iter().enumerate() {
if v.contains(&(idx as u8)) {
answered.push(Answer {
sel: Some(true),
..answer
});
} else {
unanswered.push(answer);
}
}
answered.append(&mut unanswered);
answered
}
None => answers_as_html,
};

Question {
question,
answers,
question: question_as_html,
answers: answers_as_html,
..self
}
}
Expand All @@ -132,8 +163,8 @@ impl Question {
pub fn format(self, format: QuestionFormat) -> Self {
match format {
QuestionFormat::MarkdownFull => self,
QuestionFormat::HtmlFull => self.into_html(),
QuestionFormat::HtmlShort => self.without_detailed_explanations().into_html(),
QuestionFormat::HtmlFull(v) => self.into_html(v),
QuestionFormat::HtmlShort => self.without_detailed_explanations().into_html(None),
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions vue/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bite-sized learning</title>
<script type="module" crossorigin src="/assets/index-BAz6ydKi.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bq9CWqFt.css">
<script type="module" crossorigin src="/assets/index-D-IsTMF1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-EsK9IhNe.css">
</head>
<body>
<div id="app"></div>
Expand Down
6 changes: 3 additions & 3 deletions vue/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ ul#signup-pitch li i {

.q-card input[type='radio'],
.q-card input[type='checkbox'] {
@apply h-6 w-6 checked:bg-slate-400 disabled:bg-slate-100 checked:disabled:bg-slate-400 text-green-500 p-2 my-auto mx-2;
@apply h-6 w-6 checked:bg-blue-400 disabled:bg-slate-100 checked:disabled:bg-blue-400 text-green-500 p-2 my-auto mx-2;
}

.q-card input[type='checkbox'] {
Expand Down Expand Up @@ -117,10 +117,10 @@ ul#signup-pitch li i {

.q-card pre,
.md-rendered pre {
@apply my-4;
@apply my-4 bg-slate-200 p-2 w-fit;
}

.q-card pre code,
.md-rendered pre code {
@apply bg-slate-200 p-2 font-mono text-sm rounded-none;
@apply font-mono text-sm rounded-none;
}
44 changes: 32 additions & 12 deletions vue/src/components/QuestionCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@

<div class="q-text" v-html="questionMarkdown?.question"></div>

<h3 class="mb-4">Answers</h3>
<div v-for="(answer, index) in questionMarkdown?.answers" :key="index">
<h3 v-if="isAnswered && index === 0" class="mb-4">Your answers</h3>
<h3 v-else-if="!isAnswered && index === 0" class="mb-4">Answers</h3>
<h3 v-else-if="isAnswered && index === questionMarkdown?.correct" class="mb-4">Other options</h3>

<div class="mb-8 border-2" :class="{ 'border-green-100': answer?.c, 'border-red-100': !answer?.c && isAnswered, 'border-slate-100': !isAnswered }" v-for="(answer, index) in questionMarkdown?.answers" :key="index">
<div class="flex items-center" :class="{ 'bg-green-100': answer?.c, 'bg-red-100': !answer?.c && isAnswered, 'bg-slate-100': !isAnswered }">
<div class="mb-8 border-2" :class="{ 'border-green-100': answer?.c, 'border-red-100': !answer?.c && isAnswered, 'border-slate-100': !isAnswered }">
<div class="flex items-center" :class="{ 'bg-green-100': answer?.c, 'bg-red-100': !answer?.c && isAnswered, 'bg-slate-100': !isAnswered }">

<input type="radio" v-if="questionMarkdown?.correct == 1" :name="questionMarkdown?.qid" :value="index" :disabled="isAnswered" v-model="learnerAnswerRadio" />
<input type="checkbox" v-if="questionMarkdown?.correct && questionMarkdown.correct > 1" :name="questionMarkdown?.qid" :disabled="isAnswered" :value="index" v-model="learnerAnswersCheck" />
<div class="q-answer" v-html="answer.a"></div>
<input type="radio" v-if="questionMarkdown?.correct == 1" :name="questionMarkdown?.qid" :value="index" :disabled="isAnswered" v-model="learnerAnswerRadio" />
<input type="checkbox" v-if="questionMarkdown?.correct && questionMarkdown.correct > 1" :name="questionMarkdown?.qid" :disabled="isAnswered" :value="index" v-model="learnerAnswersCheck" />
<div class="q-answer" v-html="answer.a"></div>

</div>
</div>

<div v-if="answer?.c" class="px-2">Correct.</div>
<div v-else-if="isAnswered" class="px-2">Incorrect.</div>
<div class="q-explain" v-if="answer?.e" v-html="answer.e"></div>
<div v-if="answer?.c" class="px-2">Correct.</div>
<div v-else-if="isAnswered" class="px-2">Incorrect.</div>
<div class="q-explain" v-if="answer?.e" v-html="answer.e"></div>
</div>
</div>

<div class="flex">
<div v-if="hasToken" class="flex-shrink">
<Button label="Edit" icon="pi pi-pencil" severity="secondary" rounded class="ms-4 whitespace-nowrap" @click="navigateToEditPage" />
Expand All @@ -30,7 +35,6 @@
<Button label="Submit" icon="pi pi-check" raised rounded class="font-bold px-24 py-4 my-auto whitespace-nowrap" :disabled="!isQuestionReady" @click="submitQuestion()" />
</div>
</div>

</div>
</div>
</template>
Expand Down Expand Up @@ -136,8 +140,24 @@ async function submitQuestion() {
if (response.status === 200) {
try {
// update the question with the full details
questionMarkdown.value = <Question>await response.json();
const question = <Question>await response.json();
questionMarkdown.value = question;
console.log("Full question received", questionMarkdown.value);
// reset the user selection because the answers got rearranged with the correct ones at the top
learnerAnswersCheck.value = [];
question.answers.forEach((answer, index) => {
if (answer.sel) {
if (question.correct == 1) {
learnerAnswerRadio.value = index.toString();
console.log("learnerAnswerRadio", learnerAnswerRadio.value);
} else {
learnerAnswersCheck.value.push(index.toString());
console.log("learnerAnswersCheck", learnerAnswersCheck.value);
}
}
});
} catch (error) {
console.error(error);
}
Expand Down
10 changes: 6 additions & 4 deletions vue/src/components/QuestionForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
</div>
<div class="flex gap-12">
<div class="flex-grow text-end">
<Button label="Submit" icon="pi pi-check" raised rounded class="font-bold px-24 py-4 my-auto whitespace-nowrap" :disabled="!questionReady" @click="submitQuestion()" />
<Button label="Save" icon="pi pi-check" raised rounded class="font-bold px-24 py-4 my-auto whitespace-nowrap" :disabled="!questionReady" @click="submitQuestion()" />
</div>
<div class="text-left flex-shrink">
<h4 class="mb-4">Question readiness:</h4>
Expand Down Expand Up @@ -85,8 +85,8 @@ const selectedTopic = ref(""); // the topic of the question from TOPICS
const questionText = ref(""); // the text of the question in markdown
const questionTextDebounced = ref(""); // for HTML conversion
const answers = ref<Array<Answer>>([{ a: "", e: "", c: false }]); // the list of answers
const answersDebounced = ref<Array<Answer>>([{ a: "", e: "", c: false }]); // for HTML conversion
const answers = ref<Array<Answer>>([{ a: "", e: "", c: false, sel: false }]); // the list of answers
const answersDebounced = ref<Array<Answer>>([{ a: "", e: "", c: false, sel: false }]); // for HTML conversion
const questionReady = ref(false); // enables Submit button
const mdPreviewPopover = ref();
Expand Down Expand Up @@ -131,7 +131,7 @@ const showMdPreview = (event: FocusEvent) => {
/// Adds an answer block to the form
function addAnswer(index: number) {
answers.value.splice(index + 1, 0, { a: "", e: "", c: false });
answers.value.splice(index + 1, 0, { a: "", e: "", c: false, sel: false });
}
/// Removes an answer block from the form
Expand Down Expand Up @@ -162,6 +162,8 @@ function formattingKeypress(event: KeyboardEvent) {
let formatSymbolLength = 0; // remains 0 if the selection should not be toggled
if (event.key === "b" && event.ctrlKey) { formatSymbolStart = "**"; formatSymbolEnd = "**"; formatSymbolLength = 2; }
if (event.key === "i" && event.ctrlKey) { formatSymbolStart = "_"; formatSymbolEnd = "_"; formatSymbolLength = 1; }
if (event.key === "_") { formatSymbolStart = "_"; formatSymbolEnd = "_"; }
if (event.key === "*") { formatSymbolStart = "*"; formatSymbolEnd = "*"; }
if (event.key === "`") { formatSymbolStart = "`"; formatSymbolEnd = "`"; }
if (event.key === "'") { formatSymbolStart = "'"; formatSymbolEnd = "'"; }
if (event.key === "\"") { formatSymbolStart = "\""; formatSymbolEnd = "\""; }
Expand Down
2 changes: 2 additions & 0 deletions vue/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface Answer {
e: string,
/// Set to true if correct
c: boolean,
/// Set to true if this is the user selection
sel: boolean,
}

/// A mirror of the Rust's type
Expand Down

0 comments on commit 260d912

Please sign in to comment.