Commit 794aa19f authored by Björn Eyselein's avatar Björn Eyselein
Browse files

Fixes

parent ae416d31
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
}
interface Solution {
learnerSolution: string
selectedAnswers: number[]
}
interface EditOperation {
operationType: "Replace" | "Insert" | "Delete"
index: number
char: string | null
}
interface AnswerSelectionResult {
wrong: number[],
correct: number[],
missing: number[]
}
/// <reference path="interfaces.ts"/>
interface CorrectionResult {
correct: boolean,
cardType: 'Vocable' | 'Text' | 'Blank' | 'SingleChoice' | 'MultipleChoice'
learnerSolution: Solution,
operations: EditOperation[],
answerSelection: AnswerSelectionResult
newTriesCount: number
maybeSampleSol: string | null
}
let correctionTextPar: JQuery<HTMLParagraphElement>;
let checkSolutionBtn: JQuery<HTMLButtonElement>;
let correctionTextPar: HTMLParagraphElement;
let checkSolutionBtn: HTMLButtonElement;
let checkSolutionUrl: string;
let canSolve: boolean = true;
function readSolution(cardType: string): Solution | null {
function readSolution(cardType: CardType): undefined | Solution {
switch (cardType) {
case 'Vocable':
case 'Text':
case 'Blank':
const learnerSolution: string = $('#translation_input').val() as string;
const solution: string = document.querySelector<HTMLInputElement>('#translation_input').value;
if (learnerSolution.length === 0) {
if (solution.length === 0) {
return null;
}
return {learnerSolution, selectedAnswers: []};
return {solution, selectedAnswers: []};
case 'SingleChoice':
case 'MultipleChoice':
case 'Choice':
const selectedAnswers: number[] = [];
$('input[name=choice_answers]').each((_, element: HTMLElement) => {
if (element instanceof HTMLInputElement && element.checked) {
selectedAnswers.push(parseInt(element.dataset.choiceid));
}
});
document.querySelectorAll<HTMLInputElement>('input[name=choice_answers]').forEach(
(element: HTMLElement) => {
if (element instanceof HTMLInputElement && element.checked) {
selectedAnswers.push(parseInt(element.dataset.choiceid));
}
});
if (selectedAnswers.length === 0) {
return null;
}
return {learnerSolution: "", selectedAnswers};
return {solution: "", selectedAnswers};
default:
alert('There has been an internal error: ' + cardType);
return null;
console.error('There has been an internal error: ' + cardType);
return undefined;
}
}
function onCorrectionSuccess(result: CorrectionResult): void {
function onCorrectionSuccess(result: CorrectionResult, cardType: CardType): void {
console.info(JSON.stringify(result, null, 2));
let correctionText = 'Ihre Lösung war ' + (result.correct ? '' : 'nicht ') + 'korrekt.';
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;
canSolve = !(result.correct || result.newTriesCount >= 2);
correctionTextPar.html(correctionText).removeClass(result.correct ? 'red-text' : 'green-text').addClass(result.correct ? 'green-text' : 'red-text');
correctionTextPar.innerHTML = correctionText;
correctionTextPar.classList.remove(result.correct ? 'red-text' : 'green-text');
correctionTextPar.classList.add(result.correct ? 'green-text' : 'red-text');
checkSolutionBtn.prop('disabled', result.correct || (result.newTriesCount >= 2));
checkSolutionBtn.disabled = !canSolve;
if (result.correct || result.newTriesCount >= 2) {
$('#nextFlashcardBtn').removeClass('disabled');
document.querySelector('#nextFlashcardBtn').classList.remove('disabled');
}
$('#triesSpan').text(result.newTriesCount);
document.querySelector<HTMLSpanElement>('#triesSpan').innerText = result.newTriesCount.toString();
switch (result.cardType) {
switch (cardType) {
case 'Vocable':
case 'Text':
case 'Blank':
$('#translation_input').removeClass(result.correct ? 'invalid' : 'valid').addClass(result.correct ? 'valid' : 'invalid');
const textInput = document.querySelector<HTMLInputElement>('#translation_input');
textInput.classList.remove(result.correct ? 'invalid' : 'valid');
textInput.classList.add(result.correct ? 'valid' : 'invalid');
break;
case 'SingleChoice':
case 'MultipleChoice':
console.error(JSON.stringify(result.answerSelection));
case 'Choice':
console.error(JSON.stringify(result.answersSelection));
break;
default:
console.error(result.cardType);
console.error(cardType);
}
}
function checkSolution(): void {
const cardType = $('#flashcardDiv').data('cardtype');
const cardType: CardType = document.querySelector<HTMLDivElement>('#flashcardDiv').dataset['cardtype'] as CardType;
const solution = readSolution(cardType);
const solution: 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: 'POST',
contentType: 'application/json',
data: JSON.stringify(solution),
dataType: 'json',
beforeSend: (xhr) => {
const token = $('input[name="csrfToken"]').val() as string;
xhr.setRequestHeader("Csrf-Token", token)
},
success: onCorrectionSuccess,
error: (jqXHR) => {
console.error(jqXHR.responseText);
}
})
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, cardType))
.catch(reason => {
console.error(reason)
});
}
$(() => {
correctionTextPar = $('#correctionTextPar');
checkSolutionBtn = $('#checkSolutionBtn');
checkSolutionUrl = checkSolutionBtn.data('href');
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');
checkSolutionUrl = checkSolutionBtn.dataset['href'];
$(window).bind('keypress', (event) => {
if (event.keyCode === 13) {
document.addEventListener('keypress', event => {
if (event.key === 'Enter') {
if (canSolve) {
checkSolution();
} else {
......@@ -146,4 +134,4 @@ $(() => {
}
}
});
});
\ No newline at end of file
});
// FIXME: use https://github.com/Microsoft/TypeScript/blob/b8def16e92f609327971f07232757fa6c7d29a56/lib/lib.webworker.d.ts
self.addEventListener('install', event => {
console.warn('Installing service worker...' + event);
});
self.addEventListener('fetch', event => {
console.info(event);
});
\ No newline at end of file
});
......@@ -13,114 +13,87 @@ import scala.concurrent.{ExecutionContext, Future}
class AdminController @Inject()(cc: ControllerComponents, protected val tableDefs: TableDefs)(implicit protected val ec: ExecutionContext)
extends AbstractController(cc) with ControllerHelpers with play.api.i18n.I18nSupport {
def index: EssentialAction = futureWithUser(adminRightsRequired = true) { admin =>
override protected val adminRightsRequired: Boolean = true
def index: EssentialAction = futureWithUser { admin =>
implicit request =>
tableDefs.futureAllCourses map {
allCourses => Ok(views.html.admin.adminIndex(admin, allCourses /*, FormMappings.newLanguageValuesForm*/))
allCourses => Ok(views.html.admin.adminIndex(admin, allCourses))
}
}
def courseAdmin(courseId: String): EssentialAction = futureWithUserAndCourse(adminRightsRequired = true, courseId) { (admin, course) =>
// Courses
def courseAdmin(courseId: Int): EssentialAction = futureWithUserAndCourse(courseId) { (admin, course) =>
implicit request =>
tableDefs.futureCollectionsForCourse(course) map {
tableDefs.futureAllCollectionsInCourse(course.id) map {
collections => Ok(views.html.admin.courseAdmin(admin, course, collections))
}
}
def allocateCollectionsToCourseForm(courseId: String): EssentialAction = futureWithUserAndCourse(adminRightsRequired = true, courseId) { (admin, course) =>
def newCourseForm: EssentialAction = futureWithUser { admin =>
implicit request =>
tableDefs.futureCollectionsAndCourseImportState(course) map {
collectionsAndCourseImportState => Ok(views.html.admin.allocateCollectionsToCourseForm(admin, course, collectionsAndCourseImportState))
tableDefs.futureNextCourseId.map { nextCourseId =>
Ok(views.html.forms.newCourseForm(admin, FormMappings.newCourseForm.fill(Course(nextCourseId, "", ""))))
}
}
def allocateCollectionToCourse(courseId: String, collId: Int): EssentialAction = futureWithUserAndCourse(adminRightsRequired = true, courseId) { (admin, course) =>
def newCourse: EssentialAction = futureWithUser { admin =>
implicit request =>
tableDefs.futureCollectionById(collId) flatMap {
case None => ???
case Some(collection) =>
tableDefs.allocateCollectionToCourse(course, collection) map {
case true => Redirect(routes.AdminController.allocateCollectionsToCourseForm(course.id))
case false => ???
}
def onError: Form[Course] => Future[Result] = { formWithErrors =>
Future.successful(BadRequest(views.html.forms.newCourseForm(admin, formWithErrors)))
}
}
def deallocateCollectionFromCourse(courseId: String, collId: Int) : EssentialAction = futureWithUserAndCourse(adminRightsRequired = true, courseId) {(admin, course) =>
implicit request =>
tableDefs.futureCollectionById(collId) flatMap {
case None => ???
case Some(collection) =>
tableDefs.deallocateCollectionFromCourse(course, collection) map {
case true => Redirect(routes.AdminController.allocateCollectionsToCourseForm(courseId))
case false => ???
def onRead: Course => Future[Result] = { newCourse =>
tableDefs.futureInsertCourse(newCourse) map {
_ => Redirect(routes.AdminController.courseAdmin(newCourse.id))
}
}
}
}
def languageAdmin(langId: Int): EssentialAction = futureWithUserAndLanguage(adminRightsRequired = true, langId) { (admin, language) =>
implicit request =>
???
// tableDefs.futureCollectionsForLanguage(language) map {
// collections => Ok(views.html.admin.languageAdmin(admin, language, collections, FormMappings.newCollectionValuesForm))
// }
FormMappings.newCourseForm.bindFromRequest.fold(onError, onRead)
}
def collectionAdmin(collId: Int): EssentialAction = futureWithUserAndCollection(adminRightsRequired = true, collId) { (admin, collection) =>
// Collections
def collectionAdmin(courseId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(courseId, collId) { (admin, collection) =>
implicit request =>
tableDefs.futureFlashcardsForCollection(collection) map {
flashcards => Ok(views.html.admin.collectionAdmin(admin, collection, flashcards))
flashcards => Ok(views.html.admin.collectionAdmin(admin, courseId, collection, flashcards))
}
}
def newLanguage: EssentialAction = futureWithUser(adminRightsRequired = true) { admin =>
implicit request =>
def onError: Form[String] => Future[Result] = { formWithErrors =>
tableDefs.futureAllCourses map {
allCourses => BadRequest(views.html.admin.adminIndex(admin, allCourses /*, formWithErrors*/))
}
}
def onRead: String => Future[Result] = { newLanguageName =>
val newLanguage = Language(-1, newLanguageName)
tableDefs.futureInsertLanguage(newLanguage) map {
newLangId => Redirect(routes.AdminController.languageAdmin(newLangId))
}
def newCollectionForm(courseId: Int): EssentialAction = futureWithUser { admin =>
implicit request =>
tableDefs.futureNextCollectionIdInCourse(courseId).map { nextCollectionId =>
Ok(views.html.forms.newCollectionForm(admin, courseId, FormMappings.newCollectionForm.fill(Collection(nextCollectionId, courseId, ""))))
}
FormMappings.newLanguageValuesForm.bindFromRequest.fold(onError, onRead)
}
def newCollection: EssentialAction = futureWithUser(adminRightsRequired = true) { admin =>
def newCollection(courseId: Int): EssentialAction = futureWithUser { admin =>
implicit request =>
def onError: Form[String] => Future[Result] = { formWithErrors =>
// tableDefs.futureCollectionsForLanguage(language) map {
// collections => BadRequest(views.html.admin.languageAdmin(admin, language, collections, formWithErrors))
// }
???
def onError: Form[Collection] => Future[Result] = { formWithErrors =>
Future.successful(BadRequest(views.html.forms.newCollectionForm(admin, courseId, formWithErrors)))
}
def onRead: String => Future[Result] = { newCollectionName =>
val newCollection = Collection(-1, newCollectionName)
def onRead: Collection => Future[Result] = { newCollection =>
tableDefs.futureInsertCollection(newCollection) map {
newCollId => Redirect(routes.AdminController.collectionAdmin(newCollId))
_ => Redirect(routes.AdminController.collectionAdmin(courseId, newCollection.id))
}
}
FormMappings.newCollectionValuesForm.bindFromRequest.fold(onError, onRead)
FormMappings.newCollectionForm.bindFromRequest.fold(onError, onRead)
}
def uploadCardsFile(collId: Int): EssentialAction = futureWithUserAndCollection(adminRightsRequired = true, collId) { (admin, collection) =>
def uploadCardsFile(courseId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(courseId, collId) { (admin, collection) =>
implicit request =>
request.body.asMultipartFormData flatMap (_.file(Consts.excelFileName)) match {
case None => Future(Redirect(routes.AdminController.collectionAdmin(collId)))
case None => Future(Redirect(routes.AdminController.collectionAdmin(courseId, collId)))
case Some(filePart: MultipartFormData.FilePart[TemporaryFile]) =>
val (failureStrings, importedFlashcards) = Importer.importFlashcards(collId, filePart.ref.path)
val (failureStrings, importedFlashcards) = Importer.importFlashcards(courseId, collId, filePart.ref.path)
failureStrings.foreach(println)
......@@ -129,7 +102,7 @@ class AdminController @Inject()(cc: ControllerComponents, protected val tableDef
))
futureImportedFlashcardsSaved map { importedFlashcardsSaved =>
Ok(views.html.cardPreview(admin, collection, importedFlashcards, failureStrings))
Ok(views.html.cardPreview(admin, courseId, collection, importedFlashcards, failureStrings))
}
}
......
package controllers
import javax.inject.{Inject, Singleton}
import model.jsonFormats.CourseJsonProtocol
import model.persistence.TableDefs
import play.api.Logger
import play.api.libs.json.{JsArray, JsError, JsSuccess}
import play.api.mvc.{AbstractController, ControllerComponents, EssentialAction}
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class ApiController @Inject()(cc: ControllerComponents, protected val tableDefs: TableDefs)(implicit protected val ec: ExecutionContext)
extends AbstractController(cc) with ControllerHelpers {
private val logger = Logger(classOf[ApiController])
override protected val adminRightsRequired: Boolean = false
// Routes
def courses: EssentialAction = futureWithUser { _ =>
implicit request =>
tableDefs.futureAllCourses.map {
allCourses =>
val jsonCoursesArray = JsArray(allCourses.map(CourseJsonProtocol.courseJsonFormat.writes))
Ok(jsonCoursesArray)
}
}
def course(courseId: Int): EssentialAction = withUserAndCourse(courseId) { (_, course) =>
implicit request => Ok(CourseJsonProtocol.courseJsonFormat.writes(course))
}
def newCourse: EssentialAction = futureWithUser { _ =>
implicit request =>
request.body.asJson match {
case None => Future.successful(???)
case Some(jsValue) =>
CourseJsonProtocol.courseJsonFormat.reads(jsValue) match {
case JsSuccess(newCourse, _) => tableDefs.futureInsertCourse(newCourse) map { _ => Created }
case JsError(errors) =>
errors.foreach(jsE => logger.error(jsE.toString))
Future.successful(???)
}
}
}
}
......@@ -13,38 +13,15 @@ trait ControllerHelpers extends Secured {
protected val tableDefs: TableDefs
private def onNoSuchCourse(courseId: String): Result = NotFound(s"Es gibt keinen Kurs mit der ID '$courseId'")
private def onNoSuchCourse(courseId: Int): Result = NotFound(s"Es gibt keinen Kurs mit der ID '$courseId'")
private def onNoSuchLanguage(langId: Int): Result = NotFound(s"Es gibt keine Sprache mit der ID '$langId'")
private def onNoSuchCollection(collId: Int): Result = NotFound(s"Es gibt keine Sammlung mit der ID '$collId'")
private 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 =
NotFound(s"Es gibt keine Karteikarte mit der ID '$cardId' für die Sammlung '${collection.name}'!")
protected def withUserAndLanguage(adminRightsRequired: Boolean, langId: Int)
(f: (User, Language) => Request[AnyContent] => Result): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
implicit request =>
tableDefs.futureLanguageById(langId) map {
case None => onNoSuchLanguage(langId)
case Some(language) => f(user, language)(request)
}
}
protected def futureWithUserAndLanguage(adminRightsRequired: Boolean, langId: Int)
(f: (User, Language) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future.successful(onNoSuchLanguage(langId))
case Some(language) => f(user, language)(request)
}
}
protected def withUserAndCourse(adminRightsRequired: Boolean, courseId: String)
(f: (User, Course) => Request[AnyContent] => Result): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
protected def withUserAndCourse(courseId: Int)(f: (User, Course) => Request[AnyContent] => Result): EssentialAction =
futureWithUser { user =>
implicit request =>
tableDefs.futureCourseById(courseId) map {
case None => onNoSuchCourse(courseId)
......@@ -52,9 +29,8 @@ trait ControllerHelpers extends Secured {
}
}
protected def futureWithUserAndCourse(adminRightsRequired: Boolean, courseId: String)
(f: (User, Course) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
protected def futureWithUserAndCourse(courseId: Int)(f: (User, Course) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUser { user =>
implicit request =>
tableDefs.futureCourseById(courseId) flatMap {
case None => Future.successful(onNoSuchCourse(courseId))
......@@ -63,40 +39,17 @@ trait ControllerHelpers extends Secured {
}
protected def withUserAndCollection(adminRightsRequired: Boolean, collId: Int)
(f: (User, Collection) => Request[AnyContent] => Result): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
implicit request =>
tableDefs.futureCollectionById(collId) map {
case None => onNoSuchCollection(collId)
case Some(collection) => f(user, collection)(request)
}
}
protected def futureWithUserAndCollection(adminRightsRequired: Boolean, collId: Int)
(f: (User, Collection) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
protected def futureWithUserAndCollection(courseId: Int, collId: Int)(f: (User, Collection) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUser { user =>
implicit request =>
tableDefs.futureCollectionById(collId) flatMap {
case None => Future.successful(onNoSuchCollection(collId))
tableDefs.futureCollectionById(courseId, collId) flatMap {
case None => Future.successful(onNoSuchCollection(courseId, collId))
case Some(collection) => f(user, collection)(request)
}
}
protected def withUserAndCompleteFlashcard(adminRightsRequired: Boolean, langId: Int, collId: Int, cardId: Int)
(f: (User, Collection, Flashcard) => Request[AnyContent] => Result): EssentialAction =
futureWithUserAndCollection(adminRightsRequired, collId) { (user, collection) =>
implicit request =>
tableDefs.futureFlashcardById(collection, cardId) map {
case None => onNuSuchFlashcard(collection, cardId)
case Some(flashcard) => f(user, collection, flashcard)(request)
}
}
protected def futureWithUserAndCompleteFlashcard(adminRightsRequired: Boolean, collId: Int, cardId: Int)
(f: (User, Collection, Flashcard) => Request[AnyContent] => Future[Result]): EssentialAction =
futureWithUserAndCollection(adminRightsRequired, collId) { (user, collection) =>
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))
......