Commit 20d9c163 authored by Björn Eyselein's avatar Björn Eyselein
Browse files

Update display of bucket and tries

parent 78fed46a
......@@ -82,13 +82,17 @@ function onCorrectionSuccess(result: CorrectionResult, cardType: CardType): void
checkSolutionBtn.disabled = !canSolve;
nextFlashcardBtn.disabled = canSolve;
// TODO: implement
if (!canSolve) {
answeredFlashcards++;
progressDiv.style.width = `${answeredFlashcards / maxNumOfCards * 100}%`;
document.querySelector('#answeredFcCountSpan').innerHTML = answeredFlashcards.toString();
// FIXME: disable solution inputs?
if (flashcard.cardType === 'Text' || flashcard.cardType === 'Vocable') {
document.querySelector<HTMLInputElement>('#translation_input').disabled = true;
}
if (answeredFlashcards >= maxNumOfCards) {
nextFlashcardBtn.onclick = () => {
window.location.href = nextFlashcardBtn.dataset['endurl'];
......@@ -117,12 +121,6 @@ function onCorrectionSuccess(result: CorrectionResult, cardType: CardType): void
function loadNextFlashcard(loadFlashcardUrl: string): void {
if (flashcard !== undefined && answeredFlashcards > maxNumOfCards) {
console.warn(answeredFlashcards);
} else {
console.info(answeredFlashcards);
}
fetch(loadFlashcardUrl).then(response => {
if (response.status === 200) {
response.json().then(loadedFlashcard => {
......@@ -165,14 +163,14 @@ function checkSolution(): void {
fetch(checkSolutionUrl, {method: 'PUT', body: JSON.stringify(solution), headers})
.then((response: Response) => {
if (response.status === 200) {
return response.json();
return response.json()
.then(obj => onCorrectionSuccess(obj, flashcard.cardType));
} 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)
});
......@@ -216,8 +214,6 @@ function initAll(loadNextFlashcard: (string) => void, checkSolution: () => void)
maxNumOfCards = parseInt(document.querySelector<HTMLSpanElement>('#maxCardCountSpan').innerText);
console.warn(maxNumOfCards);
progressDiv = document.querySelector<HTMLDivElement>('#progressDiv');
document.addEventListener('keypress', event => {
......@@ -225,7 +221,6 @@ function initAll(loadNextFlashcard: (string) => void, checkSolution: () => void)
if (canSolve) {
checkSolutionBtn.click();
} else {
console.info("Clicking nextFlashcardBtn");
nextFlashcardBtn.click();
}
}
......
......@@ -44,6 +44,9 @@ interface Flashcard {
blanksAnswers: BlanksAnswerFragment[];
choiceAnswers: ChoiceAnswer[];
currentTries: number;
currentBucket: undefined | number;
}
interface Solution {
......
......@@ -43,6 +43,14 @@ function updateView(flashcard: Flashcard): void {
updateQuestionText(flashcard);
document.querySelector<HTMLSpanElement>('#triesSpan').innerText = flashcard.currentTries.toFixed(0);
if (flashcard.currentBucket !== undefined) {
document.querySelector<HTMLSpanElement>('#bucketSpan').innerText = flashcard.currentBucket.toFixed(0);
} else {
document.querySelector<HTMLSpanElement>('#bucketSpan').innerText = '--';
}
// Set answering inputs
const answerDiv = document.querySelector<HTMLDivElement>('#answerDiv');
switch (flashcard.cardType) {
......
......@@ -73,14 +73,16 @@ class AdminController @Inject()(cc: ControllerComponents, protected val tableDef
CollectionBasics(nextCollectionId, courseId, allLanguages.head.id, allLanguages.head.id, "")
)
Ok(views.html.forms.newCollectionForm(admin, courseId, filledForm))
Ok(views.html.forms.newCollectionForm(admin, courseId, filledForm, allLanguages))
}
}
def newCollection(courseId: Int): EssentialAction = futureWithUser { admin =>
implicit request =>
def onError: Form[CollectionBasics] => Future[Result] = { formWithErrors =>
Future.successful(BadRequest(views.html.forms.newCollectionForm(admin, courseId, formWithErrors)))
tableDefs.futureAllLanguages.map {
allLanguages => BadRequest(views.html.forms.newCollectionForm(admin, courseId, formWithErrors, allLanguages))
}
}
def onRead: CollectionBasics => Future[Result] = { newCollection =>
......
......@@ -3,18 +3,16 @@ package controllers
import javax.inject.{Inject, Singleton}
import model._
import model.persistence.TableDefs
import play.api.Logger
import play.api.libs.json.JsString
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
@Singleton
class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs: TableDefs)(implicit protected val ec: ExecutionContext)
extends AbstractController(cc) with ControllerHelpers with play.api.i18n.I18nSupport {
private val logger = Logger(classOf[HomeController])
// private val logger = Logger(classOf[HomeController])
override protected val adminRightsRequired: Boolean = false
......@@ -114,16 +112,13 @@ class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs
case None => ???
case Some(flashcard) =>
tableDefs.futureUserAnswerForFlashcard(user, flashcard, solution.frontToBack).flatMap {
tableDefs.futureUserAnswerForFlashcard(user, flashcard.cardId, flashcard.collId, flashcard.courseId, solution.frontToBack).flatMap {
maybePreviousAnswer: Option[UserAnsweredFlashcard] =>
Corrector.completeCorrect(user, solution, flashcard, maybePreviousAnswer) match {
case Failure(exception) => Future.successful(BadRequest(exception.getMessage))
case Success((corrResult, newAnswer)) =>
val (corrResult, newAnswer) = Corrector.completeCorrect(user, solution, flashcard, maybePreviousAnswer)
tableDefs.futureInsertOrUpdateUserAnswer(newAnswer).map { _ =>
Ok(JsonFormats.completeCorrectionResultFormat.writes(corrResult))
}
tableDefs.futureInsertOrUpdateUserAnswer(newAnswer).map { _ =>
Ok(JsonFormats.completeCorrectionResultFormat.writes(corrResult))
}
}
......
......@@ -13,5 +13,8 @@ final case class FlashcardToAnswer(
frontHint: Option[String],
frontToBack: Boolean,
blanksAnswerFragments: Seq[BlanksAnswerFragment] = Seq.empty,
choiceAnswers: Seq[ChoiceAnswer] = Seq.empty
choiceAnswers: Seq[ChoiceAnswer] = Seq.empty,
currentTries: Int = 0,
currentBucket: Option[Int]
)
......@@ -5,7 +5,8 @@ object Consts {
val answerName : String = "answer"
val answerSelectionName: String = "answerSelection"
val backName: String = "back"
val backLanguageIdName: String = "backLanguageId"
val backName : String = "back"
val cardTypeName : String = "cardType"
val charName : String = "char"
......@@ -15,7 +16,8 @@ object Consts {
val excelFileName: String = "excelFile"
val frontName: String = "front"
val frontLanguageIdName: String = "frontLanguageId"
val frontName : String = "front"
val idName : String = "id"
val indexName: String = "index"
......@@ -45,7 +47,6 @@ object Consts {
val shortNameName : String = "shortName"
val titleName: String = "title"
val triesName: String = "tries"
val usernameName: String = "username"
......
package model
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import model.levenshtein.Levenshtein
import scala.util.{Failure, Success, Try}
object Corrector {
private val maxBucketId: Int = 6
......@@ -29,7 +26,7 @@ object Corrector {
private def correctFlashcard(flashcard: Flashcard, solution: Solution): CorrectionResult = flashcard.cardType match {
case CardType.Vocable | CardType.Text =>
val sampleSolution = if(solution.frontToBack) flashcard.back else flashcard.front
val sampleSolution = if (solution.frontToBack) flashcard.back else flashcard.front
val editOperations = Levenshtein.calculateBacktrace(solution.solution, sampleSolution)
CorrectionResult(editOperations.isEmpty, operations = editOperations)
......@@ -42,7 +39,7 @@ object Corrector {
CorrectionResult(answerSelectionResult.isCorrect, answersSelection = Some(answerSelectionResult))
}
def completeCorrect(user: User, solution: Solution, flashcard: Flashcard, maybePreviousDbAnswer: Option[UserAnsweredFlashcard]): Try[(CorrectionResult, UserAnsweredFlashcard)] = {
def completeCorrect(user: User, solution: Solution, flashcard: Flashcard, maybePreviousDbAnswer: Option[UserAnsweredFlashcard]): (CorrectionResult, UserAnsweredFlashcard) = {
val correctionResult = correctFlashcard(flashcard, solution)
......@@ -52,36 +49,25 @@ object Corrector {
maybePreviousDbAnswer match {
case None =>
val newTries = 0
Success((
val newTries = if (isCorrect) 0 else 1
(
correctionResult.copy(newTriesCount = newTries),
UserAnsweredFlashcard(user.username, flashcard.cardId, flashcard.collId, flashcard.courseId, bucket = 0, today, isCorrect, tries = newTries, solution.frontToBack)
))
UserAnsweredFlashcard(user.username, flashcard.cardId, flashcard.collId, flashcard.courseId, bucket = 0, today, isCorrect, wrongTries = newTries, solution.frontToBack)
)
case Some(oldAnswer) =>
val daysSinceLastAnswer: Long = Math.abs(ChronoUnit.DAYS.between(today, oldAnswer.dateAnswered))
val isTryInNewBucket = daysSinceLastAnswer >= Math.pow(3, oldAnswer.bucket)
val newBucket = Math.min(if (isCorrect) oldAnswer.bucket + 1 else oldAnswer.bucket, maxBucketId)
if (!isTryInNewBucket && (oldAnswer.correct || oldAnswer.tries >= 2)) {
Failure(new Exception("More than 2 tries already..."))
} else {
val newBucket = Math.min(if (isCorrect) oldAnswer.bucket + 1 else oldAnswer.bucket, maxBucketId)
val triesToAdd = if (isCorrect) 0 else 1
val oldTries = if (oldAnswer.isActive) oldAnswer.wrongTries else 0
val newTries: Int = if (isTryInNewBucket) {
0
} else if (isCorrect) {
oldAnswer.tries
} else {
oldAnswer.tries + 1
}
val newTries = oldTries + triesToAdd
Success((
correctionResult.copy(newTriesCount = newTries, maybeSampleSolution = None),
oldAnswer.copy(bucket = newBucket, dateAnswered = today, correct = isCorrect, tries = newTries)
))
}
(
correctionResult.copy(newTriesCount = newTries, maybeSampleSolution = None),
oldAnswer.copy(bucket = newBucket, dateAnswered = today, correct = isCorrect, wrongTries = newTries)
)
}
}
......
......@@ -60,8 +60,8 @@ object FormMappings {
mapping(
idName -> number,
courseIdName -> number,
"frontLanguageId" -> number,
"backLanguageId" -> number,
frontLanguageIdName -> number,
backLanguageIdName -> number,
nameName -> nonEmptyText
)(CollectionBasics.apply)(CollectionBasics.unapply)
)
......
......@@ -95,10 +95,8 @@ final case class FlashcardIdentifier(cardId: Int, collId: Int, courseId: Int) {
// User answered flashcard
final case class UserAnsweredFlashcard(username: String, cardId: Int, collId: Int, courseId: Int, bucket: Int, dateAnswered: LocalDate, correct: Boolean, tries: Int, frontToBack: Boolean) {
final case class UserAnsweredFlashcard(username: String, cardId: Int, collId: Int, courseId: Int, bucket: Int, dateAnswered: LocalDate, correct: Boolean, wrongTries: Int, frontToBack: Boolean) {
// def cardIdentifier: FlashcardIdentifier = FlashcardIdentifier(cardId, collId, courseId)
def isActive: Boolean = dateAnswered.until(LocalDate.now(), ChronoUnit.DAYS) < Math.pow(3, bucket - 1)
lazy val isActive: Boolean = dateAnswered.until(LocalDate.now(), ChronoUnit.DAYS) < Math.pow(3, bucket - 1)
}
......@@ -48,7 +48,7 @@ class TableDefs @Inject()(override protected val dbConfigProvider: DatabaseConfi
def correct: Rep[Boolean] = column[Boolean](correctName)
def tries: Rep[Int] = column[Int](triesName)
def wrongTries: Rep[Int] = column[Int]("wrong_tries")
def pk: PrimaryKey = primaryKey("uaf_pk", (username, cardId, collId))
......@@ -58,7 +58,7 @@ class TableDefs @Inject()(override protected val dbConfigProvider: DatabaseConfi
def cardFk: ForeignKeyQuery[FlashcardsTable, DBFlashcard] = foreignKey("uaf_card_fk", (cardId, collId), flashcardsTQ)(fc => (fc.id, fc.collId))
override def * : ProvenShape[UserAnsweredFlashcard] = (username, cardId, collId, courseId, bucket, dateAnswered, correct, tries, frontToBack) <> (UserAnsweredFlashcard.tupled, UserAnsweredFlashcard.unapply)
override def * : ProvenShape[UserAnsweredFlashcard] = (username, cardId, collId, courseId, bucket, dateAnswered, correct, wrongTries, frontToBack) <> (UserAnsweredFlashcard.tupled, UserAnsweredFlashcard.unapply)
}
......
......@@ -14,22 +14,38 @@ trait TableQueries {
private def flashcardToDoFilter(fctd: FlashcardToDoView[_ <: FlashcardToAnswerData], collection: Collection, user: User): Rep[Boolean] =
fctd.collId === collection.id && fctd.courseId === collection.courseId && fctd.username === user.username
def futureFlashcardToAnswerById(courseId: Int, collId: Int, cardId: Int, frontToBack: Boolean): Future[Option[FlashcardToAnswer]] = {
def futureFlashcardToAnswerById(user: User, courseId: Int, collId: Int, cardId: Int, frontToBack: Boolean): Future[Option[FlashcardToAnswer]] = {
val dbFlashcardByIdQuery = flashcardsTQ.filter {
fc => fc.id === cardId && fc.collId === collId && fc.courseId === courseId
}.result.headOption
db.run(dbFlashcardByIdQuery) flatMap {
db.run(dbFlashcardByIdQuery).flatMap {
case None => Future.successful(None)
case Some(DBFlashcard(_, _, _, cardType, front, frontHint, back, backHint)) =>
for {
choiceAnswersForDBFlashcard <- choiceAnswersForFlashcard(cardId, collId, courseId)
blanksAnswersForDbFlashcard <- blanksAnswersForFlashcard(cardId, collId, courseId)
maybeOldAnswer <- futureUserAnswerForFlashcard(user, cardId, collId, courseId, frontToBack)
} yield {
val frontToSend = if (frontToBack) front else back
val frontHintToSend = if (frontToBack) frontHint else backHint
Some(FlashcardToAnswer(cardId, collId, courseId, cardType, frontToSend, frontHintToSend, frontToBack, blanksAnswersForDbFlashcard, choiceAnswersForDBFlashcard))
val currentTries = maybeOldAnswer.map {
oldAnswer => if (oldAnswer.isActive) oldAnswer.wrongTries else 0
}.getOrElse(0)
val currentBucket = maybeOldAnswer.map(_.bucket)
Some(
FlashcardToAnswer(
cardId, collId, courseId, cardType,
frontToSend, frontHintToSend, frontToBack,
blanksAnswersForDbFlashcard,
choiceAnswersForDBFlashcard,
currentTries,
currentBucket
)
)
}
}
}
......@@ -43,14 +59,14 @@ trait TableQueries {
db.run(flashcardsToLearnTQ.filter(flashcardToDoFilter(_, collection, user)).result.headOption).flatMap {
case None => Future.successful(None)
case Some(FlashcardToAnswerData(cardId, collId, courseId, _, frontToBack)) =>
futureFlashcardToAnswerById(courseId, collId, cardId, frontToBack)
futureFlashcardToAnswerById(user, courseId, collId, cardId, frontToBack)
}
def futureMaybeNextFlashcardToRepeat(user: User): Future[Option[FlashcardToAnswer]] =
db.run(flashcardsToRepeatTQ.filter(_.username === user.username).result.headOption).flatMap {
case None => Future.successful(None)
case Some(FlashcardToAnswerData(cardId, collId, courseId, _, frontToBack)) =>
futureFlashcardToAnswerById(courseId, collId, cardId, frontToBack)
futureFlashcardToAnswerById(user, courseId, collId, cardId, frontToBack)
}
def futureFlashcardsToRepeatCount(user: User): Future[Int] = db.run(
......@@ -66,11 +82,11 @@ trait TableQueries {
// Queries - UserAnsweredFlashcard
def futureUserAnswerForFlashcard(user: User, flashcard: Flashcard, frontToBack: Boolean): Future[Option[UserAnsweredFlashcard]] =
def futureUserAnswerForFlashcard(user: User, cardId: Int, collId: Int, courseId: Int, frontToBack: Boolean): Future[Option[UserAnsweredFlashcard]] =
db.run(
usersAnsweredFlashcardsTQ
.filter {
uaf => uaf.username === user.username && uaf.cardId === flashcard.cardId && uaf.collId === flashcard.collId && uaf.frontToBack === frontToBack
uaf => uaf.username === user.username && uaf.cardId === cardId && uaf.collId === collId && uaf.courseId === courseId && uaf.frontToBack === frontToBack
}
.result.headOption)
......
@import model.{User, CollectionBasics, Consts}
@import model.{User, CollectionBasics, Consts, Language}
@(user: User, courseId: Int, newCollectionForm: Form[CollectionBasics])(implicit requestHeader: RequestHeader, messagesProvider: MessagesProvider)
@(user: User, courseId: Int, newCollectionForm: Form[CollectionBasics], allLanguages: Seq[Language])(implicit requestHeader: RequestHeader, messagesProvider: MessagesProvider)
@title = @{
"Neue Sammlung erstellen"
}
@title = @{
"Neue Sammlung erstellen"
}
@main(title, Some(user)) {
<h2 class="center-align">@title</h2>
@main(title, Some(user)) {
<h2 class="center-align">@title</h2>
<div class="row">
<div class="col s12">
@helper.form(routes.AdminController.newCollection(courseId)) {
<div class="row">
<div class="col s12">
@helper.form(routes.AdminController.newCollection(courseId)) {
@helper.CSRF.formField
@helper.CSRF.formField
@myhelpers.input(newCollectionForm(Consts.idName), labelContent = "Id", isRequired = true, isReadOnly = true)
@myhelpers.input(newCollectionForm(Consts.idName), labelContent = "Id", isRequired = true, isReadOnly = true)
@myhelpers.input(newCollectionForm(Consts.courseIdName), labelContent = "Kurs", isRequired = true, isReadOnly = true)
@myhelpers.input(newCollectionForm(Consts.courseIdName), labelContent = "Kurs", isRequired = true, isReadOnly = true)
@myhelpers.input(newCollectionForm(Consts.nameName), labelContent = "Name", isRequired = true)
@myhelpers.input(newCollectionForm(Consts.nameName), labelContent = "Name", isRequired = true)
<button class="btn btn-large waves-effect waves-block @accentColor">Sammlung erstellen</button>
@myhelpers.select(newCollectionForm(Consts.frontLanguageIdName), labelContent = "Sprache Vorderseiten",
options = allLanguages.map(l => (l.id.toString, l.name)))
}
</div>
@myhelpers.select(newCollectionForm(Consts.backLanguageIdName), labelContent = "Sprache Rückseiten",
options = allLanguages.map(l => (l.id.toString, l.name)))
<button class="btn btn-large waves-effect waves-block @accentColor">Sammlung erstellen</button>
}
</div>
</div>
}
}
......@@ -53,8 +53,8 @@
Stapel:
<span id="bucketSpan">--</span>
</code>
<code>
Versuche:
<code title="Maximal 2 erlaubt">
Fehlversuche:
<span id="triesSpan">0</span>
</code>
</p>
......
......@@ -85,6 +85,7 @@
<script>
$(document).ready(function () {
$('.sidenav').sidenav();
$('select').formSelect();
});
</script>
......
@(field: Field, labelContent: String, fieldType: String = "text", inputWidth: Int = 12, placeHolder: Option[String] = None,
@(field: Field, labelContent: String, fieldType: String = "text", placeHolder: Option[String] = None,
isRequired: Boolean = false, isReadOnly: Boolean = false
)
......@@ -7,7 +7,7 @@
}
<div class="row">
<div class="input-field col s@inputWidth">
<div class="input-field col s12">
<input type="@fieldType" id="@field.id" name="@field.name" class="validate" placeholder="@labelContent" @attributes>
<label for="@field.id">@labelContent</label>
</div>
......
@(field: Field, labelContent: String, options: Seq[String], inputWidth: Int = 12, isRequired: Boolean = false)
@(field: Field, labelContent: String, options: Seq[(String, String)], isRequired: Boolean = false)
@attributes = {
@if(isRequired) { required }
}
@attributes = {
@if(isRequired) { required }
}
<div class="input-field col s@inputWidth">
<select id="@field.id" name="@field.name" @attributes>
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
<label for="@field.id">@labelContent</label>
</div>
@defaultSelectValue = @{
if(field.value.isEmpty) "selected" else ""
}
<div class="row">
<div class="input-field col s12">
<select id="@field.id" name="@field.name" @attributes>
<option value="" @defaultSelectValue>Bitte wählen</option>
@for((optionValue, optionText) <- options;
selectStr = if(field.value.contains(optionValue)) "selected" else "") {
<option value="@optionValue"@selectStr>@optionText</option>
}
</select>
<label for="@field.id">@labelContent</label>
</div>
</div>
......@@ -117,7 +117,7 @@ create table if not exists users_answered_flashcards (
bucket int not null,
date_answered date not null,
correct boolean not null default false,
tries int not null default 1,
wrong_tries int not null default 0,
constraint bucket_check check (bucket between 0 and 6),
......@@ -161,7 +161,7 @@ select f.id as card_id,
from flashcards f
left join users_answered_flashcards uaf on uaf.card_id = f.id and uaf.coll_id = f.coll_id
where datediff(now(), date_answered) >= power(3, bucket)
or (uaf.correct = false and uaf.tries < 2)
or (uaf.correct = false and uaf.wrong_tries < 2)
order by time_since_answered, front_to_back;
# --- !Downs
......
......@@ -21,11 +21,13 @@ insert into users_in_courses(username, course_id)
values ('first_user', 1);
insert into collections (id, course_id, front_language_id, back_language_id, name)
values (1, 1, 1, 2, 'La nature et la géographie'),
(2, 1, 1, 2, 'Les plantes'),
(3, 1, 1, 2, 'Les animaux'),
(4, 1, 1, 2, 'L''être humain'),
(5, 1, 1, 2, 'La famille');
values (1, 1, 1, 2, 'La nature et la géographie')
# ,
# (2, 1, 1, 2, 'Les plantes'),
# (3, 1, 1, 2, 'Les animaux'),
# (4, 1, 1, 2, 'L''être humain'),
# (5, 1, 1, 2, 'La famille')
;
# --- !Downs
......@@ -35,6 +37,9 @@ from collections;
delete ignore
from users_in_courses;
delete ignore
from languages;
delete ignore
from courses;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment