Commit 4efbb1b8 authored by Björn Eyselein's avatar Björn Eyselein
Browse files

Repeating updated

parent e580e3c6
type CardType = 'Vocable' | 'Text' | 'Blank' | 'Choice';
function shuffleArray<T>(array: T[]): T[] {
let newArray: T[] = array.slice(0);
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
function domReady(callBack: () => void): void {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callBack);
} else {
callBack();
}
}
interface BlanksAnswerFragment {
answerId: number;
answer: string;
}
interface ChoiceAnswer {
answerId: number;
answer: string;
correct: boolean;
}
interface Flashcard {
cardId: number;
collId: number;
courseId: number;
cardType: CardType;
question: string;
questionHint: string | undefined;
meaning: string;
meaningHint: string | undefined;
blanksAnswers: BlanksAnswerFragment[];
choiceAnswers: ChoiceAnswer[];
}
interface Solution {
cardId: number
collId: number
courseId: number
solution: string
selectedAnswers: number[]
}
interface EditOperation {
operationType: "Replace" | "Insert" | "Delete"
index: number
char: string | null
}
interface AnswerSelectionResult {
wrong: number[]
correct: number[]
missing: number[]
}
interface CorrectionResult {
correct: boolean
operations: EditOperation[]
answersSelection: AnswerSelectionResult
newTriesCount: number
maybeSampleSol: string | null
}
type CardType = 'Vocable' | 'Text' | 'Blank' | 'Choice';
interface Solution {
solution: string
selectedAnswers: number[]
}
interface EditOperation {
operationType: "Replace" | "Insert" | "Delete"
index: number
char: string | null
}
interface AnswerSelectionResult {
wrong: number[],
correct: number[],
missing: number[]
}
interface CorrectionResult {
correct: boolean,
operations: EditOperation[],
answersSelection: AnswerSelectionResult
newTriesCount: number
maybeSampleSol: string | null
}
/// <reference path="interfaces.ts"/>
/// <reference path="helpers.ts"/>
let correctionTextPar: HTMLParagraphElement;
let checkSolutionBtn: HTMLButtonElement;
let cardId: number;
let collId: number;
let courseId: number;
let checkSolutionUrl: string;
let canSolve: boolean = true;
function readSolution(cardType: CardType): undefined | Solution {
switch (cardType) {
case 'Vocable':
......@@ -15,7 +21,7 @@ function readSolution(cardType: CardType): undefined | Solution {
return null;
}
return {solution, selectedAnswers: []};
return {cardId, collId, courseId, solution, selectedAnswers: []};
case 'Blank':
throw cardType;
......@@ -35,7 +41,7 @@ function readSolution(cardType: CardType): undefined | Solution {
return null;
}
return {solution: "", selectedAnswers};
return {cardId, collId, courseId, solution: "", selectedAnswers};
default:
console.error('There has been an internal error: ' + cardType);
return undefined;
......@@ -114,17 +120,13 @@ function checkSolution(): void {
});
}
function domReady(callBack: () => void): void {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callBack);
} else {
callBack();
}
}
domReady(() => {
correctionTextPar = document.querySelector<HTMLParagraphElement>('#correctionTextPar');
checkSolutionBtn = document.querySelector<HTMLButtonElement>('#checkSolutionBtn');
cardId = parseInt(checkSolutionBtn.dataset['cardid']);
collId = parseInt(checkSolutionBtn.dataset['collid']);
courseId = parseInt(checkSolutionBtn.dataset['courseid']);
checkSolutionUrl = checkSolutionBtn.dataset['href'];
document.addEventListener('keypress', event => {
......
/// <reference path="helpers.ts"/>
const textAnswerInput: string = `
<div class="row">
<div class="input-field col s12">
<input type="text" id="translation_input" autofocus autocomplete="off">
<label for="translation_input">Übersetzung</label>
</div>
</div>`.trim();
function buildAnswerFragments(answerFragments: BlanksAnswerFragment[]): string {
return answerFragments.map(answerFragment => `
<div class="row">
<div class="input-field col s12">
<input type="text" id="blanksInput_${answerFragment.answerId}" autofocus autocomplete="off">
<label for="blanksInput_${answerFragment.answerId}">Übersetzung</label>
</div>
</div>`.trim()).join('\n');
}
function buildChoiceAnswers(choiceAnswers: ChoiceAnswer[]): string {
const choiceInputType: string = choiceAnswers.filter(ca => ca.correct).length > 0 ? 'radio' : 'checkbox';
return shuffleArray(choiceAnswers).map(choiceAnswer => `
<p>
<label for="choice_${choiceAnswer.answerId}">
<input id="choice_${choiceAnswer.answerId}" name="choice_answers" type="${choiceInputType}" data-choiceid="${choiceAnswer.answerId}">
<span>${choiceAnswer.answer}</span>
</label>
</p>`.trim()).join('\n');
}
function updateQuestionText(flashcard: Flashcard): void {
let questionText = flashcard.question;
if (flashcard.questionHint !== undefined) {
questionText += ` <i>${flashcard.questionHint}</i>`;
}
document.querySelector<HTMLHeadingElement>('#questionDiv').innerHTML = questionText;
}
function updateView(flashcard: Flashcard): void {
updateQuestionText(flashcard);
// Set answering inputs
const answerDiv = document.querySelector<HTMLDivElement>('#answerDiv');
switch (flashcard.cardType) {
case 'Text':
case 'Vocable' :
answerDiv.innerHTML = textAnswerInput;
document.querySelector<HTMLInputElement>('#translation_input').focus();
break;
case 'Blank':
answerDiv.innerHTML = buildAnswerFragments(flashcard.blanksAnswers);
break;
case 'Choice':
answerDiv.innerHTML = buildChoiceAnswers(flashcard.choiceAnswers);
break;
}
}
/// <reference path="helpers.ts"/>
/// <reference path="questionMaker.ts"/>
let correctionTextPar: HTMLParagraphElement;
let checkSolutionBtn: HTMLButtonElement;
let nextFlashcardBtn: HTMLButtonElement;
let initialLoadBtn: HTMLButtonElement;
let checkSolutionUrl: string;
let canSolve: boolean = true;
let flashcard: Flashcard;
let repeatedFlashcards: number = 0;
function readSolution(cardType: CardType): undefined | Solution {
let solution: string = '';
let selectedAnswers: number[] = [];
switch (cardType) {
case 'Vocable':
case 'Text':
solution = document.querySelector<HTMLInputElement>('#translation_input').value;
if (solution.length === 0) {
return null;
}
break;
case 'Blank':
throw cardType;
// return {solution: '', selectedAnswers: []};
// break;
case 'Choice':
selectedAnswers = Array.from(document.querySelectorAll<HTMLInputElement>('input[name=choice_answers]'))
.filter((element: HTMLInputElement) => element.checked)
.map((element: HTMLInputElement) => parseInt(element.dataset.choiceid));
if (selectedAnswers.length === 0) {
alert('Bitte wählen Sie mindestens eine Antwort aus.');
return null;
}
break;
default:
console.error('There has been an internal error: ' + cardType);
return undefined;
}
return {
cardId: flashcard.cardId,
collId: flashcard.collId,
courseId: flashcard.courseId,
solution,
selectedAnswers
};
}
function onCorrectionSuccess(result: CorrectionResult, cardType: CardType): void {
console.info(JSON.stringify(result, null, 2));
let correctionText = `Ihre Lösung war ${result.correct ? '' : 'nicht '} korrekt.`;
if ((result.newTriesCount >= 2) && (result.maybeSampleSol != null)) {
correctionText += ` Die korrekte Lösung lautet '<code>${result.maybeSampleSol}</code>'.`;
}
canSolve = !(result.correct || result.newTriesCount >= 2);
correctionTextPar.innerHTML = correctionText;
correctionTextPar.classList.remove(result.correct ? 'red-text' : 'green-text');
correctionTextPar.classList.add(result.correct ? 'green-text' : 'red-text');
// Update buttons
checkSolutionBtn.disabled = !canSolve;
nextFlashcardBtn.disabled = canSolve;
document.querySelector<HTMLSpanElement>('#triesSpan').innerText = result.newTriesCount.toString();
switch (cardType) {
case 'Vocable':
case 'Text':
case 'Blank':
const textInput = document.querySelector<HTMLInputElement>('#translation_input');
textInput.classList.remove(result.correct ? 'invalid' : 'valid');
textInput.classList.add(result.correct ? 'valid' : 'invalid');
break;
case 'Choice':
console.error(JSON.stringify(result.answersSelection));
break;
default:
console.error(cardType);
}
}
function checkSolution(): void {
const solution: Solution = readSolution(flashcard.cardType);
if (solution === null) {
alert("Sie können keine leere Lösung abgeben!");
return;
}
const headers: Headers = new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
'Csrf-Token': document.querySelector<HTMLInputElement>('input[name="csrfToken"]').value
});
fetch(checkSolutionUrl, {method: 'PUT', body: JSON.stringify(solution), headers})
.then((response: Response) => {
if (response.status === 200) {
return response.json();
} else {
response.text().then(text => console.error(text));
return Promise.reject("Error code was " + response.status);
}
}
)
.then(obj => onCorrectionSuccess(obj, flashcard.cardType))
.catch(reason => {
console.error(reason)
});
}
function loadNextFlashcard(): void {
if (flashcard !== undefined && repeatedFlashcards++ > 10) {
console.warn(repeatedFlashcards);
}
const loadFlashcardUrl: string = initialLoadBtn.dataset['href'];
fetch(loadFlashcardUrl)
.then(response => {
if (response.status === 200) {
return response.json().then(loadedFlashcard => {
flashcard = loadedFlashcard;
canSolve = true;
// Update buttons
checkSolutionBtn.disabled = !canSolve;
nextFlashcardBtn.disabled = canSolve;
correctionTextPar.innerHTML = '&nbsp;';
updateView(flashcard);
});
} else if (response.status === 404) {
// FIXME Keine weitere Karteikarte mehr...
alert("Sie haben alle Karteikarten wiederholt...");
window.location.href = '/';
}
}
)
.catch(reason => console.error(reason));
}
function performInitialFlashcardLoad(): void {
initialLoadBtn.remove();
loadNextFlashcard();
}
domReady(() => {
initialLoadBtn = document.querySelector<HTMLButtonElement>('#loadFlashcardButton');
initialLoadBtn.click();
correctionTextPar = document.querySelector<HTMLParagraphElement>('#correctionTextPar');
nextFlashcardBtn = document.querySelector<HTMLButtonElement>('#nextFlashcardBtn');
checkSolutionBtn = document.querySelector<HTMLButtonElement>('#checkSolutionBtn');
checkSolutionUrl = checkSolutionBtn.dataset['href'];
document.addEventListener('keypress', event => {
if (event.key === 'Enter') {
if (canSolve) {
checkSolution();
} else {
document.getElementById('nextFlashcardBtn').click();
}
}
});
});
......@@ -13,11 +13,11 @@ trait ControllerHelpers extends Secured {
protected val tableDefs: TableDefs
private def onNoSuchCourse(courseId: Int): Result = NotFound(s"Es gibt keinen Kurs mit der ID '$courseId'")
protected def onNoSuchCourse(courseId: Int): Result = NotFound(s"Es gibt keinen Kurs mit der ID '$courseId'")
private def onNoSuchCollection(courseId: Int, collId: Int): Result = NotFound(s"Es gibt keine Sammlung mit der ID '$collId' für den Kurs '$courseId'")
protected def onNoSuchCollection(courseId: Int, collId: Int): Result = NotFound(s"Es gibt keine Sammlung mit der ID '$collId' für den Kurs '$courseId'")
private def onNuSuchFlashcard(collection: Collection, cardId: Int): Result =
protected def onNoSuchFlashcard(collection: Collection, cardId: Int): Result =
NotFound(s"Es gibt keine Karteikarte mit der ID '$cardId' für die Sammlung '${collection.name}'!")
protected def withUserAndCourse(courseId: Int)(f: (User, Course) => Request[AnyContent] => Result): EssentialAction =
......@@ -48,14 +48,4 @@ trait ControllerHelpers extends Secured {
}
}
protected def futureWithUserAndCompleteFlashcard(courseId: Int, collId: Int, cardId: Int)(f: (User, Collection, Flashcard) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUserAndCollection(courseId, collId) { (user, collection) =>
implicit request =>
tableDefs.futureFlashcardById(collection, cardId) flatMap {
case None => Future.successful(onNuSuchFlashcard(collection, cardId))
case Some(flashcard) => f(user, collection, flashcard)(request)
}
}
}
......@@ -75,7 +75,6 @@ class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs
} yield Ok(views.html.collection(user, courseId, collection, flashcardCount, toLearnCount, toRepeatCount))
}
def startLearning(courseId: Int, collId: Int, isRepeating: Boolean): EssentialAction = futureWithUserAndCollection(courseId, collId) { (user, collection) =>
implicit request =>
val futureFlashcard = if (isRepeating)
......@@ -89,68 +88,91 @@ class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs
}
}
def learn(courseId: Int, collId: Int, cardId: Int, isRepeating: Boolean): EssentialAction =
futureWithUserAndCompleteFlashcard(courseId, collId, cardId) { (user, _, flashcard) =>
implicit request =>
def learn(courseId: Int, collId: Int, cardId: Int, isRepeating: Boolean): EssentialAction = futureWithUser { user =>
implicit request =>
tableDefs.futureFlashcardById(courseId, collId, cardId).flatMap {
case None => Future.successful(onNoSuchFlashcard(collection = ???, cardId))
case Some(flashcard) =>
tableDefs.futureUserAnswerForFlashcard(user, flashcard).map { maybeOldAnswer =>
if (!isRepeating && maybeOldAnswer.isDefined) {
// TODO: Something went wrong, take next flashcard?
Redirect(routes.HomeController.startLearning(courseId, collId, isRepeating))
} else {
Ok(views.html.learn(user, flashcard, maybeOldAnswer, isRepeating))
tableDefs.futureUserAnswerForFlashcard(user, flashcard).map { maybeOldAnswer =>
if (!isRepeating && maybeOldAnswer.isDefined) {
// TODO: Something went wrong, take next flashcard?
Redirect(routes.HomeController.startLearning(courseId, collId, isRepeating))
} else {
Ok(views.html.learn(user, flashcard, maybeOldAnswer, isRepeating))
}
}
}
}
}
}
def repeat: EssentialAction = withUser { user =>
implicit request => Ok(views.html.repeat(user))
}
def checkSolution(courseId: Int, collId: Int, cardId: Int): EssentialAction = futureWithUserAndCompleteFlashcard(courseId, collId, cardId) { (user, _, flashcard) =>
def nextFlashcardToRepeat: EssentialAction = futureWithUser { user =>
implicit request =>
tableDefs.futureMaybeNextFlashcardToRepeat(user).map {
case None => NotFound("There has been an error?")
case Some(fc) => Ok(JsonFormats.flashcardFormat.writes(fc))
}
}
def checkRepeatSolution: EssentialAction = ???
def checkSolution(): EssentialAction = futureWithUser { user =>
implicit request =>
request.body.asJson flatMap (json => JsonFormats.solutionFormat.reads(json).asOpt) match {
case None => Future(BadRequest(JsString("Could not read solution...")))
case None => Future.successful(BadRequest(JsString("Could not read solution...")))
case Some(solution) =>
tableDefs.futureUserAnswerForFlashcard(user, flashcard).flatMap { maybePreviousAnswer: Option[UserAnsweredFlashcard] =>
tableDefs.futureFlashcardById(solution.courseId, solution.collId, solution.cardId).flatMap {
case None => ???
case Some(flashcard) =>
val previousTries = maybePreviousAnswer.map(_.tries).getOrElse(0)
tableDefs.futureUserAnswerForFlashcard(user, flashcard).flatMap { maybePreviousAnswer: Option[UserAnsweredFlashcard] =>
if (previousTries >= 2) {
Future.successful(BadRequest(JsString("More than 2 tries already...")))
} else {
val previousTries = maybePreviousAnswer.map(_.tries).getOrElse(0)
Corrector.correct(flashcard, solution) match {
case CorrectionResult(correct, operations, answersSelection) =>
if (previousTries >= 2) {
Future.successful(BadRequest(JsString("More than 2 tries already...")))
} else {
val today = LocalDate.now()
Corrector.correct(flashcard, solution) match {
case CorrectionResult(correct, operations, answersSelection) =>
val newAnswer: UserAnsweredFlashcard = maybePreviousAnswer match {
case None => UserAnsweredFlashcard(user.username, cardId, collId, courseId, bucket = 0, today, correct, tries = 0)
case Some(oldAnswer) =>
val today = LocalDate.now()
val newBucket = if (correct) oldAnswer.bucket + 1 else oldAnswer.bucket
val newAnswer: UserAnsweredFlashcard = maybePreviousAnswer match {
case None => UserAnsweredFlashcard(user.username, flashcard.cardId, flashcard.collId, flashcard.courseId, bucket = 0, today, correct, tries = 0)
case Some(oldAnswer) =>
val daysSinceLastAnswer: Long = ChronoUnit.DAYS.between(today, oldAnswer.dateAnswered)
val newBucket = if (correct) oldAnswer.bucket + 1 else oldAnswer.bucket
val newTries: Int = if (daysSinceLastAnswer > Math.pow(3, oldAnswer.bucket)) {
0
} else if (correct) {
oldAnswer.tries
} else {
oldAnswer.tries + 1
}
val daysSinceLastAnswer: Long = ChronoUnit.DAYS.between(today, oldAnswer.dateAnswered)
oldAnswer.copy(bucket = newBucket, dateAnswered = today, correct = correct, tries = newTries)
}
val newTries: Int = if (daysSinceLastAnswer > Math.pow(3, oldAnswer.bucket)) {
0
} else if (correct) {
oldAnswer.tries
} else {
oldAnswer.tries + 1
}
tableDefs.futureInsertOrUpdateUserAnswer(newAnswer) map {
_ =>
val completeCorrectionResult = CompleteCorrectionResult(correct, operations, answersSelection, newTriesCount = newAnswer.tries, maybeSampleSolution = None)
oldAnswer.copy(bucket = newBucket, dateAnswered = today, correct = correct, tries = newTries)
}
Ok(JsonFormats.completeCorrectionResultFormat.writes(completeCorrectionResult))
tableDefs.futureInsertOrUpdateUserAnswer(newAnswer) map {
_ =>
val completeCorrectionResult = CompleteCorrectionResult(correct, operations, answersSelection, newTriesCount = newAnswer.tries, maybeSampleSolution = None)
Ok(JsonFormats.completeCorrectionResultFormat.writes(completeCorrectionResult))
}
}
}
}
}
}
}
}
}
......
......@@ -2,7 +2,7 @@ package model
import model.levenshtein.EditOperation
final case class Solution(solution: String, selectedAnswers: Seq[Int])
final case class Solution(cardId: Int, collId: Int, courseId: Int