Commit 14a1a761 authored by Björn Eyselein's avatar Björn Eyselein
Browse files

Use redirect for learning, correction implemented

parent 5c6a055d
/// <reference path="loadFlashcards.ts"/>
/// <reference path="solutionTypes.ts"/>
interface Solution {
learnerSolution: string
selectedAnswers: number[]
}
interface EditOperation {
operationType: "Replace" | "Insert" | "Delete"
index: number
char: string | null
}
let checkSolutionUrl: string;
interface AnswerSelectionResult {
wrong: number[],
correct: number[],
missing: number[]
}
interface CorrectionResult {
correct: boolean,
cardType: 'Vocable' | 'Text' | 'SingleChoice' | 'MultipleChoice'
learnerSolution: Solution,
operations: EditOperation[],
answerSelection: AnswerSelectionResult
}
function checkSolution(): void {
let answer: AnswerType;
let correctionTextPar: JQuery<HTMLParagraphElement>;
let checkSolutionBtn: JQuery<HTMLButtonElement>;
let checkSolutionUrl: string;
switch (currentFlashcard.cardType) {
case 'vocable':
function readSolution(cardType: string): Solution | null {
switch (cardType) {
case 'Vocable':
case 'Text':
const learnerSolution: string = $('#translation_input').val() as string;
if (learnerSolution.length === 0) {
alert('Lösung ist leer!');
return;
return null;
}
answer = new TranslationSolution(learnerSolution);
break;
return {learnerSolution, selectedAnswers: []};
case 'single_choice':
case 'multiple_choice':
case 'SingleChoice':
case 'MultipleChoice':
const selectedAnswers: number[] = [];
$('input[name=choice_answers]').each((_, element: HTMLElement) => {
......@@ -32,31 +50,67 @@ function checkSolution(): void {
});
if (selectedAnswers.length === 0) {
alert('Sie haben keine Lösung ausgewählt!');
return;
return null;
}
answer = new ChoiceSolution(selectedAnswers);
return {learnerSolution: "", selectedAnswers};
default:
alert('There has been an internal error: ' + cardType);
return null;
}
}
function onCorrectionSuccess(result: CorrectionResult): void {
console.info(JSON.stringify(result, null, 2));
correctionTextPar.prop('hidden', false).text('Ihre Lösung war ' + (result.correct ? '' : 'nicht ') + 'korrekt.');
if (result.correct) {
checkSolutionBtn.prop('disabled', true);
$('#nextFlashcardBtn').removeClass('disabled');
} else {
}
switch (result.cardType) {
case 'Vocable':
case 'Text':
$('#translation_input').removeClass(result.correct ? 'invalid' : 'valid').addClass(result.correct ? 'valid' : 'invalid');
break;
case 'SingleChoice':
case 'MultipleChoice':
console.error(JSON.stringify(result.answerSelection));
break;
default:
alert('There has been an internal error...');
return;
console.error(result.cardType);
}
}
let solution: Solution = new Solution(currentFlashcard.id, currentFlashcard.collId, currentFlashcard.langId, currentFlashcard.cardType, answer);
console.warn(JSON.stringify(solution, null, 2));
function checkSolution(): void {
const cardType = $('#flashcardDiv').data('cardtype');
const solution = readSolution(cardType);
if (solution === null) {
alert("Sie können keine leere Lösung abgeben!");
return;
}
// console.warn(JSON.stringify(solution, null, 2));
$.ajax({
url: checkSolutionUrl,
method: 'PUT',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(solution),
dataType: 'json',
success: (result: CorrectionResult) => {
console.info(JSON.stringify(result, null, 2));
$('#translation_input').removeClass(result.correct ? 'invalid' : 'valid').addClass(result.correct ? 'valid' : 'invalid');
beforeSend: (xhr) => {
const token = $('input[name="csrfToken"]').val() as string;
xhr.setRequestHeader("Csrf-Token", token)
},
success: onCorrectionSuccess,
error: (jqXHR) => {
console.error(jqXHR.responseText);
}
......@@ -64,21 +118,8 @@ function checkSolution(): void {
}
function nextFlashcard(): void {
console.error("TODO: next flashcard...");
updateHtml();
}
$(() => {
const startLearningBtn = $('#startLearningBtn');
const loadFlashcardsUrl = startLearningBtn.data('href');
startLearningBtn.remove();
checkSolutionUrl = $('#checkSolutionBtn').data('href');
$.ajax({
url: loadFlashcardsUrl,
success: onLoadFlashcardsSuccess,
error: onLoadFlashcardsError
});
correctionTextPar = $('#correctionTextPar');
checkSolutionBtn = $('#checkSolutionBtn');
checkSolutionUrl = checkSolutionBtn.data('href');
});
\ No newline at end of file
interface LoadedFlashcardAnswer {
id: number
flashcardId: number
collId: number
langId: number
answer: string
correctness: 'CORRECT' | 'OPTIONAL' | 'WRONG'
}
interface LoadedFlashcard {
id: number
collId: number
langId: number
cardType: 'vocable' | 'single_choice' | 'multiple_choice'
question: string
meaning: string | null
answers: LoadedFlashcardAnswer[]
}
let flashcards: LoadedFlashcard[] = [];
let currentFlashcard: LoadedFlashcard | null = null;
let flashcardDiv: JQuery<HTMLElement>;
function buildInputForVocable(/*vocableFlashcard: LoadedFlashcard*/): string {
return `
<div class="row">
<div class="input-field col s12">
<input type="text" id="translation_input">
<label for="translation_input">Übersetzung</label>
</div>
</div>`.trim();
}
function buildInputForChoiceFlashcard(choiceFlashcard: LoadedFlashcard): string {
const inputType: string = choiceFlashcard.cardType == 'single_choice' ? 'radio' : 'checkbox';
let answerInputs: string[] = [];
for (const answer of choiceFlashcard.answers) {
answerInputs.push(`
<p>
<label for="choice_${answer.id}">
<input id="choice_${answer.id}" name="choice_answers" type="${inputType}" data-choiceid="${answer.id}">
<span>${answer.answer}</span>
</label>
</p>`.trim());
}
return answerInputs.join('\n');
}
function updateHtml(): void {
[currentFlashcard, ...flashcards] = flashcards;
if (currentFlashcard != undefined) {
let toAppend: string = `
<div class="card-panel">
<h4 class="center-align">${currentFlashcard.question}</h4>
</div>`.trim();
if (currentFlashcard.cardType == 'vocable') {
toAppend += buildInputForVocable(/*currentFlashcard*/);
} else if (currentFlashcard.cardType === 'single_choice' || currentFlashcard.cardType === 'multiple_choice') {
toAppend += buildInputForChoiceFlashcard(currentFlashcard);
}
flashcardDiv.html(toAppend);
}
}
function onLoadFlashcardsSuccess(loadedFlashcards: LoadedFlashcard[]): void {
flashcards = loadedFlashcards;
flashcardDiv = $('#flashcardDiv');
// console.info(JSON.stringify(loadedFlashcards, null, 2));
updateHtml();
}
function onLoadFlashcardsError(jqXHR): void {
console.error(jqXHR.responseText);
}
\ No newline at end of file
interface AnswerType {
}
class Solution {
constructor(public cardId: number, public collId: number, public langId: number,
public cardType: 'vocable' | 'single_choice' | 'multiple_choice',
public answer: AnswerType) {
}
}
class TranslationSolution implements AnswerType {
constructor(public solution: string) {
}
}
class ChoiceSolution implements AnswerType {
constructor(public selectedAnswers: number[]) {
}
}
interface CorrectionResult {
correct: boolean,
learnerSolution: Solution,
sampleSolution: string
}
package controllers
import javax.inject.{Inject, Singleton}
import model.{Language, TableDefs}
import play.api.mvc.{AbstractController, ControllerComponents, EssentialAction, Result}
import model._
import play.api.libs.json.{JsError, JsSuccess}
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs: TableDefs)(implicit ec: ExecutionContext)
extends AbstractController(cc) with Secured {
extends AbstractController(cc) with Secured with play.api.i18n.I18nSupport {
// Helper methods
private def onNoSuchLanguage(langId: Int): Result = NotFound(s"Es gibt keine Sprache mit der ID $langId")
private def onNoSuchCollection(language: Language, collId: Int): Result =
NotFound(s"Es gibt keine Sammlung mit der ID $collId für die Sprache ${language.name}")
private def onNuSuchFlashcard(language: Language, collection: Collection, cardId: Int): Result =
NotFound(s"Es gibt keine Karteikarte mit der ID $cardId für die Sammlung ${collection.name} für die Sprache ${language.name}")
private def futureWithUserAndLanguage(langId: Int)(f: (User, Language) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUser { user =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
case Some(language) => f(user, language)(request)
}
}
// private def withUserLanguageAndCollection(langId: Int, collId: Int)(f: (User, Language, Collection) => Request[AnyContent] => Result): EssentialAction =
// futureWithUserAndLanguage(langId) { (user, language) =>
// implicit request =>
// tableDefs.futureCollectionById(language, collId) map {
// case None => onNoSuchCollection(language, collId)
// case Some(collection) => f(user, language, collection)(request)
// }
// }
private def futureWithUserAndCollection(langId: Int, collId: Int)(f: (User, Language, Collection) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.futureCollectionById(language, collId) flatMap {
case None => Future(onNoSuchCollection(language, collId))
case Some(collection) => f(user, language, collection)(request)
}
}
private def withUserAndCompleteFlashcard(langId: Int, collId: Int, cardId: Int)
(f: (User, Language, Collection, CompleteFlashcard) => Request[AnyContent] => Result): EssentialAction =
futureWithUserAndCollection(langId, collId) { (user, language, collection) =>
implicit request =>
tableDefs.futureFlashcardById(collection, cardId) flatMap {
case None => Future(onNuSuchFlashcard(language, collection, cardId))
case Some(flashcard) => tableDefs.futureChoiceAnswersForFlashcard(flashcard) map {
answers => f(user, language, collection, CompleteFlashcard(flashcard, answers))(request)
}
}
}
private def futureWithUserAndCompleteFlashcard(langId: Int, collId: Int, cardId: Int)
(f: (User, Language, Collection, CompleteFlashcard) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUserAndCollection(langId, collId) { (user, language, collection) =>
implicit request =>
tableDefs.futureFlashcardById(collection, cardId) flatMap {
case None => Future(onNuSuchFlashcard(language, collection, cardId))
case Some(flashcard) => tableDefs.futureChoiceAnswersForFlashcard(flashcard) flatMap {
answers => f(user, language, collection, CompleteFlashcard(flashcard, answers))(request)
}
}
}
// Routes
def index: EssentialAction = futureWithUser { user =>
implicit request =>
tableDefs.futureLanguagesForUser(user) map { languages =>
......@@ -29,73 +88,74 @@ class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs
}
}
def language(langId: Int): EssentialAction = futureWithUser { user =>
def language(langId: Int): EssentialAction = futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
case Some(language) => tableDefs.futureCollectionsForLanguage(language) map {
collections => Ok(views.html.language(user, language, collections))
}
tableDefs.futureCollectionsForLanguage(language) map {
collections => Ok(views.html.language(user, language, collections))
}
}
def selectLanguage(langId: Int): EssentialAction = futureWithUser { user =>
def selectLanguage(langId: Int): EssentialAction = futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
case Some(language) => tableDefs.activateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
tableDefs.activateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
}
def deselectLanguage(langId: Int): EssentialAction = futureWithUser { user =>
def deselectLanguage(langId: Int): EssentialAction = futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
case Some(language) => tableDefs.deactivateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
tableDefs.deactivateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
}
def collection(langId: Int, collId: Int): EssentialAction = futureWithUser { user =>
def collection(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(langId, collId) { (user, language, collection) =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
case Some(language) => tableDefs.futureCollectionById(language, collId) flatMap {
case None => Future(onNoSuchCollection(language, collId))
case Some(collection) =>
for {
flashcardCount <- tableDefs.futureFlashcardCountForCollection(collection)
toLearnCount <- tableDefs.futureFlashcardsToLearnCount(user, collection)
toRepeatCount <- tableDefs.futureFlashcardsToRepeatCount(user, collection)
} yield Ok(views.html.collection(user, language, collection, flashcardCount, toLearnCount, toRepeatCount))
}
}
for {
flashcardCount <- tableDefs.futureFlashcardCountForCollection(collection)
toLearnCount <- tableDefs.futureFlashcardsToLearnCount(user, collection)
toRepeatCount <- tableDefs.futureFlashcardsToRepeatCount(user, collection)
} yield Ok(views.html.collection(user, language, collection, flashcardCount, toLearnCount, toRepeatCount))
}
def learn(langId: Int, collId: Int): EssentialAction = futureWithUser { user =>
def startLearning(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(langId, collId) { (user, _, collection) =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
case Some(language) => tableDefs.futureCollectionById(language, collId) flatMap {
case None => Future(onNoSuchCollection(language, collId))
case Some(collection) => tableDefs.futureFlashcardsToLearnCount(user, collection) map {
case 0 => ???
case _ => Ok(views.html.learn(user, language, collection))
}
}
tableDefs.futureMaybeIdentifierNextFlashcardToLearn(user, collection) map {
case None => ???
case Some(identifier) => Redirect(routes.HomeController.learn(identifier.langId, identifier.collId, identifier.cardId))
}
}
def learn(langId: Int, collId: Int, cardId: Int): EssentialAction = withUserAndCompleteFlashcard(langId, collId, cardId) { (user, language, collection, completeFlashcard) =>
implicit request => Ok(views.html.learn(user, completeFlashcard))
}
def repeat(langId: Int, collId: Int): EssentialAction = futureWithUser { user =>
def startRepeating(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(langId, collId) { (_, _, _) =>
implicit request =>
???
}
def repeat(langId: Int, collId: Int, cardId: Int): EssentialAction = withUserAndCompleteFlashcard(langId, collId, cardId) { (_, _, _, _) =>
implicit request =>
???
}
def checkSolution(langId: Int, collId: Int, cardId: Int): EssentialAction = futureWithUserAndCompleteFlashcard(langId, collId, cardId) { (user, _, _, completeFlashcard) =>
implicit request =>
request.body.asJson match {
case None => ???
case Some(json) => JsonFormats.solutionFormat.reads(json) match {
case JsError(_) => ???
case JsSuccess(solution, _) =>
val correctionResult = Corrector.correct(completeFlashcard, solution)
tableDefs.futureInsertOrUpdateUserAnswer(user, completeFlashcard.flashcard, correctionResult.correct) map {
_ => Ok(JsonFormats.correctionResultWrites.writes(correctionResult))
}
}
}
}
}
......@@ -32,11 +32,10 @@ class LoginController @Inject()(cc: ControllerComponents, val dbConfigProvider:
tableDefs.futureSaveUser(newUser) flatMap {
case false => Future(BadRequest("Could not save user!"))
case true =>
tableDefs.savePwHash(pwHash) map {
_ => Redirect(routes.LoginController.loginForm())
// Ok(views.html.registered.render(credentials.username))
}
case true => tableDefs.savePwHash(pwHash) map {
_ => Redirect(routes.LoginController.loginForm())
// Ok(views.html.registered.render(credentials.username))
}
}
}
......@@ -51,20 +50,17 @@ class LoginController @Inject()(cc: ControllerComponents, val dbConfigProvider:
val onRead: LoginFormValues => Future[Result] = { credentials =>
val futureUserAndPwHash: Future[(Option[User], Option[UserPassword])] = for {
user <- tableDefs.futureUserByUserName(credentials.username)
pwHash <- tableDefs.futurePwHashForUser(credentials.username)
} yield (user, pwHash)
futureUserAndPwHash map {
case (None, _) => Redirect(controllers.routes.LoginController.register())
case (Some(_), None) => BadRequest("Cannot change password!")
case (Some(user), Some(pwHash)) =>
if (credentials.password isBcrypted pwHash.pwHash) {
Redirect(controllers.routes.HomeController.index()).withSession(idName -> user.username)
} else {
Ok(views.html.forms.loginForm(FormMappings.loginValuesForm.fill(credentials)))
}
tableDefs.futureUserByUserName(credentials.username) flatMap {
case None => Future(Redirect(controllers.routes.LoginController.registerForm()))
case Some(user) => tableDefs.futurePwHashForUser(credentials.username) map {
case None => BadRequest("Cannot change password!")
case Some(userPassword) =>
if (credentials.password isBcrypted userPassword.pwHash) {
Redirect(controllers.routes.HomeController.index()).withSession(idName -> user.username)
} else {
Ok(views.html.forms.loginForm(FormMappings.loginValuesForm.fill(credentials)))
}
}
}
}
......
......@@ -19,12 +19,6 @@ trait Secured {
private def futureOnUnauthorized(request: RequestHeader)(implicit ec: ExecutionContext): Future[Result] =
Future(onUnauthorized(request))
// private def onInsufficientPrivileges(request: RequestHeader): Result = Redirect(routes.HomeController.index()).flashing("msg" -> "You do not have sufficient privileges!")
// private def futureOnInsufficientPrivileges(request: RequestHeader)(implicit ec: ExecutionContext): Future[Result] =
// Future(onInsufficientPrivileges(request))
private def withAuth(f: => String => Request[AnyContent] => Future[Result]): EssentialAction =
Security.Authenticated(username, onUnauthorized)(user => controllerComponents.actionBuilder.async(request => f(user)(request)))
......@@ -59,24 +53,4 @@ trait Secured {
}
}
// def withAdmin(f: User => Request[AnyContent] => Result)(implicit ec: ExecutionContext): EssentialAction = withAuth { username =>
// implicit request =>
// tableDefs.userByName(username) map {
// case Some(user) =>
// if (user.isAdmin) f(user)(request)
// else onInsufficientPrivileges(request)
// case None => onUnauthorized(request)
// }
// }
// def futureWithAdmin(f: User => Request[AnyContent] => Future[Result])(implicit ec: ExecutionContext): EssentialAction = withAuth { username =>
// implicit request =>
// tableDefs.userByName(username) flatMap {
// case Some(user) =>
// if (user.isAdmin) f(user)(request)
// else futureOnInsufficientPrivileges(request)
// case None => futureOnUnauthorized(request)
// }
// }
}
\ No newline at end of file
......@@ -2,25 +2,38 @@ package model
object Consts {
val answerName: String = "answer"
val answerName : String = "answer"
val answerSelectionName: String = "answerSelection"
val cardTypeName : String = "cardType"
val charName : String = "char"
val correctName : String = "correct"
val correctnessName: String = "correctness"
val idName: String = "id"
val idName : String = "id"
val indexName: String = "index"
val meaningName: String = "meaning"
val missingName: String = "missing"
val nameName: String = "name"
val operationsName : String = "operations"
val operationTypeName: String = "operationType"
val pwName: String = "pw"
val questionName: String = "question"