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

Einlesen von Karteikarten aus Excel-Dateien

parent 193395e1
package controllers
import javax.inject.{Inject, Singleton}
import model._
import play.api.data.Form
import play.api.libs.Files.TemporaryFile
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
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 =>
implicit request =>
tableDefs.futureAllLanguages map {
allLanguages => Ok(views.html.admin.adminIndex(admin, allLanguages, FormMappings.newLanguageValuesForm))
}
}
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))
}
}
def collectionAdmin(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(adminRightsRequired = true, langId, collId) { (admin, language, collection) =>
implicit request =>
tableDefs.futureFlashcardsForCollection(collection) map {
flashcards => Ok(views.html.admin.collectionAdmin(admin, language, collection, flashcards))
}
}
def newLanguage: EssentialAction = futureWithUser(adminRightsRequired = true) { admin =>
implicit request =>
def onError: Form[String] => Future[Result] = { formWithErrors =>
tableDefs.futureAllLanguages map {
allLanguages => BadRequest(views.html.admin.adminIndex(admin, allLanguages, formWithErrors))
}
}
def onRead: String => Future[Result] = { newLanguageName =>
val newLanguage = Language(-1, newLanguageName)
tableDefs.futureInsertLanguage(newLanguage) map {
newLangId => Redirect(routes.AdminController.languageAdmin(newLangId))
}
}
FormMappings.newLanguageValuesForm.bindFromRequest.fold(onError, onRead)
}
def newCollection(langId: Int): EssentialAction = futureWithUserAndLanguage(adminRightsRequired = true, langId) { (admin, language) =>
implicit request =>
def onError: Form[String] => Future[Result] = { formWithErrors =>
tableDefs.futureCollectionsForLanguage(language) map {
collections => BadRequest(views.html.admin.languageAdmin(admin, language, collections, formWithErrors))
}
}
def onRead: String => Future[Result] = { newCollectionName =>
val newCollection = Collection(-1, langId, newCollectionName)
tableDefs.futureInsertCollection(newCollection) map {
newCollId => Redirect(routes.AdminController.collectionAdmin(langId, newCollId))
}
}
FormMappings.newCollectionValuesForm.bindFromRequest.fold(onError, onRead)
}
def uploadCardsFile(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(adminRightsRequired = true, langId, collId) { (admin, language, collection) =>
implicit request =>
request.body.asMultipartFormData flatMap (_.file(Consts.excelFileName)) match {
case None => Future(Redirect(routes.AdminController.collectionAdmin(langId, collId)))
case Some(filePart: MultipartFormData.FilePart[TemporaryFile]) =>
val (failureStrings, importedFlashcards) = Importer.importFlashcards(langId, collId, filePart.ref.path)
val futureImportedFlashcardsSaved = Future.sequence(importedFlashcards.map(
tableDefs.futureInsertCompleteFlashcard
))
futureImportedFlashcardsSaved map { importedFlashcardsSaved =>
Ok(views.html.cardPreview(admin, language, collection, importedFlashcardsSaved, failureStrings))
}
}
}
}
......@@ -18,9 +18,19 @@ trait ControllerHelpers extends Secured {
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}")
protected def futureWithUserAndLanguage(langId: Int)(f: (User, Language) => Request[AnyContent] => Future[Result])
(implicit ec: ExecutionContext): EssentialAction =
futureWithUser { user =>
protected def withUserAndLanguage(adminRightsRequired: Boolean, langId: Int)(f: (User, Language) => Request[AnyContent] => Result)
(implicit ec: ExecutionContext): 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])
(implicit ec: ExecutionContext): EssentialAction =
futureWithUser(adminRightsRequired) { user =>
implicit request =>
tableDefs.futureLanguageById(langId) flatMap {
case None => Future(onNoSuchLanguage(langId))
......@@ -28,9 +38,20 @@ trait ControllerHelpers extends Secured {
}
}
protected def futureWithUserAndCollection(langId: Int, collId: Int)(f: (User, Language, Collection) => Request[AnyContent] => Future[Result])
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndLanguage(langId) { (user, language) =>
protected def withUserAndCollection(adminRightsRequired: Boolean, langId: Int, collId: Int)(f: (User, Language, Collection) => Request[AnyContent] => Result)
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndLanguage(adminRightsRequired, langId) { (user, language) =>
implicit request =>
tableDefs.futureCollectionById(language, collId) map {
case None => onNoSuchCollection(language, collId)
case Some(collection) => f(user, language, collection)(request)
}
}
protected def futureWithUserAndCollection(adminRightsRequired: Boolean, langId: Int, collId: Int)(f: (User, Language, Collection) => Request[AnyContent] => Future[Result])
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndLanguage(adminRightsRequired, langId) { (user, language) =>
implicit request =>
tableDefs.futureCollectionById(language, collId) flatMap {
case None => Future(onNoSuchCollection(language, collId))
......@@ -38,10 +59,10 @@ trait ControllerHelpers extends Secured {
}
}
protected def withUserAndCompleteFlashcard(langId: Int, collId: Int, cardId: Int)
(f: (User, Language, Collection, CompleteFlashcard) => Request[AnyContent] => Result)
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndCollection(langId, collId) { (user, language, collection) =>
protected def withUserAndCompleteFlashcard(adminRightsRequired: Boolean, langId: Int, collId: Int, cardId: Int)
(f: (User, Language, Collection, CompleteFlashcard) => Request[AnyContent] => Result)
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndCollection(adminRightsRequired, langId, collId) { (user, language, collection) =>
implicit request =>
tableDefs.futureFlashcardById(collection, cardId) flatMap {
case None => Future(onNuSuchFlashcard(language, collection, cardId))
......@@ -51,10 +72,10 @@ trait ControllerHelpers extends Secured {
}
}
protected def futureWithUserAndCompleteFlashcard(langId: Int, collId: Int, cardId: Int)
(f: (User, Language, Collection, CompleteFlashcard) => Request[AnyContent] => Future[Result])
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndCollection(langId, collId) { (user, language, collection) =>
protected def futureWithUserAndCompleteFlashcard(adminRightsRequired: Boolean, langId: Int, collId: Int, cardId: Int)
(f: (User, Language, Collection, CompleteFlashcard) => Request[AnyContent] => Future[Result])
(implicit ec: ExecutionContext): EssentialAction =
futureWithUserAndCollection(adminRightsRequired, langId, collId) { (user, language, collection) =>
implicit request =>
tableDefs.futureFlashcardById(collection, cardId) flatMap {
case None => Future(onNuSuchFlashcard(language, collection, cardId))
......
package controllers
import javax.inject.{Inject, Singleton}
import model.{Corrector, JsonFormats, TableDefs}
import model._
import play.api.libs.json.{JsError, JsSuccess}
import play.api.mvc._
import scala.concurrent.ExecutionContext
@Singleton
class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs: TableDefs)(implicit ec: ExecutionContext)
class HomeController @Inject()(cc: ControllerComponents, protected val tableDefs: TableDefs)(implicit protected val ec: ExecutionContext)
extends AbstractController(cc) with ControllerHelpers with play.api.i18n.I18nSupport {
// Routes
def index: EssentialAction = futureWithUser { user =>
def index: EssentialAction = futureWithUser(adminRightsRequired = false) { user =>
implicit request =>
tableDefs.futureLanguagesForUser(user) map { languages =>
Ok(views.html.myLanguages(user, languages))
}
}
def allLanguages: EssentialAction = futureWithUser { user =>
def allLanguages: EssentialAction = futureWithUser(adminRightsRequired = false) { user =>
implicit request =>
tableDefs.futureLanguagesAndUserLearns(user) map { languagesAndUserLearns =>
Ok(views.html.allLanguages(user, languagesAndUserLearns))
}
}
def language(langId: Int): EssentialAction = futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.futureCollectionsForLanguage(language) map {
collections => Ok(views.html.language(user, language, collections))
}
}
def selectLanguage(langId: Int): EssentialAction = futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.activateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
}
def deselectLanguage(langId: Int): EssentialAction = futureWithUserAndLanguage(langId) { (user, language) =>
implicit request =>
tableDefs.deactivateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
}
def collection(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(langId, collId) { (user, language, collection) =>
implicit request =>
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 startLearning(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(langId, collId) { (user, _, collection) =>
implicit request =>
tableDefs.futureMaybeIdentifierNextFlashcardToLearn(user, collection) map {
case None => Redirect(routes.HomeController.collection(langId, collId))
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, _, _, completeFlashcard) =>
implicit request => Ok(views.html.learn(user, completeFlashcard, isRepeating = false))
}
def startRepeating(langId: Int, collId: Int): EssentialAction = futureWithUserAndCollection(langId, collId) { (user, _, collection) =>
implicit request =>
tableDefs.futureMaybeIdentifierNextFlashcardToRepeat(user, collection) map {
case None => Redirect(routes.HomeController.collection(langId, collId))
case Some(identifier) => Redirect(routes.HomeController.repeat(identifier.langId, identifier.collId, identifier.cardId))
}
}
def repeat(langId: Int, collId: Int, cardId: Int): EssentialAction = withUserAndCompleteFlashcard(langId, collId, cardId) { (user, _, _, completeFlashcard) =>
implicit request => Ok(views.html.learn(user, completeFlashcard, isRepeating = false))
}
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))
}
def language(langId: Int): EssentialAction =
futureWithUserAndLanguage(adminRightsRequired = false, langId) { (user, language) =>
implicit request =>
tableDefs.futureCollectionsForLanguage(language) map {
collections => Ok(views.html.language(user, language, collections))
}
}
}
}
def selectLanguage(langId: Int): EssentialAction =
futureWithUserAndLanguage(adminRightsRequired = false, langId) { (user, language) =>
implicit request =>
tableDefs.activateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
}
def deselectLanguage(langId: Int): EssentialAction =
futureWithUserAndLanguage(adminRightsRequired = false, langId) { (user, language) =>
implicit request =>
tableDefs.deactivateLanguageForUser(user, language) map {
case true => Redirect(routes.HomeController.allLanguages())
case false => ???
}
}
def collection(langId: Int, collId: Int): EssentialAction =
futureWithUserAndCollection(adminRightsRequired = false, langId, collId) { (user, language, collection) =>
implicit request =>
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 startLearning(langId: Int, collId: Int): EssentialAction =
futureWithUserAndCollection(adminRightsRequired = false, langId, collId) { (user, _, collection) =>
implicit request =>
tableDefs.futureMaybeIdentifierNextFlashcardToLearn(user, collection) map {
case None => Redirect(routes.HomeController.collection(langId, collId))
case Some(identifier) => Redirect(routes.HomeController.learn(identifier.langId, identifier.collId, identifier.cardId))
}
}
def learn(langId: Int, collId: Int, cardId: Int): EssentialAction =
withUserAndCompleteFlashcard(adminRightsRequired = false, langId, collId, cardId) { (user, _, _, completeFlashcard) =>
implicit request => Ok(views.html.learn(user, completeFlashcard, isRepeating = false))
}
def startRepeating(langId: Int, collId: Int): EssentialAction =
futureWithUserAndCollection(adminRightsRequired = false, langId, collId) { (user, _, collection) =>
implicit request =>
tableDefs.futureMaybeIdentifierNextFlashcardToRepeat(user, collection) map {
case None => Redirect(routes.HomeController.collection(langId, collId))
case Some(identifier) => Redirect(routes.HomeController.repeat(identifier.langId, identifier.collId, identifier.cardId))
}
}
def repeat(langId: Int, collId: Int, cardId: Int): EssentialAction =
withUserAndCompleteFlashcard(adminRightsRequired = false, langId, collId, cardId) { (user, _, _, completeFlashcard) =>
implicit request => Ok(views.html.learn(user, completeFlashcard, isRepeating = false))
}
def checkSolution(langId: Int, collId: Int, cardId: Int): EssentialAction =
futureWithUserAndCompleteFlashcard(adminRightsRequired = false, 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))
}
}
}
}
}
......@@ -27,14 +27,13 @@ class LoginController @Inject()(cc: ControllerComponents, val dbConfigProvider:
}
val onRead: RegisterFormValues => Future[Result] = { credentials =>
val newUser = User(credentials.username, credentials.name)
val newUser = User(credentials.username)
val pwHash = UserPassword(credentials.username, credentials.pw.bcrypt)
tableDefs.futureSaveUser(newUser) flatMap {
case false => Future(BadRequest("Could not save user!"))
case true => tableDefs.savePwHash(pwHash) map {
case true => tableDefs.futureSavePwHash(pwHash) map {
_ => Redirect(routes.LoginController.loginForm())
// Ok(views.html.registered.render(credentials.username))
}
}
}
......
......@@ -10,47 +10,39 @@ import scala.concurrent.{ExecutionContext, Future}
trait Secured {
self: AbstractController =>
implicit protected val ec: ExecutionContext
protected val tableDefs: TableDefs
private def username(request: RequestHeader): Option[String] = request.session.get(idName)
private def onUnauthorized(request: RequestHeader): Result = Redirect(controllers.routes.LoginController.loginForm()).withNewSession
private def futureOnUnauthorized(request: RequestHeader)(implicit ec: ExecutionContext): Future[Result] =
private def futureOnUnauthorized(request: RequestHeader): Future[Result] =
Future(onUnauthorized(request))
private def withAuth(f: => String => Request[AnyContent] => Future[Result]): EssentialAction =
Security.Authenticated(username, onUnauthorized)(user => controllerComponents.actionBuilder.async(request => f(user)(request)))
private def withAuthWithBodyParser[A](bodyParser: BodyParser[A])(f: => String => Request[A] => Future[Result]): EssentialAction =
Security.Authenticated(username, onUnauthorized)(user => controllerComponents.actionBuilder.async(bodyParser)(request => f(user)(request)))
protected def withUser(f: User => Request[AnyContent] => Result)(implicit ec: ExecutionContext): EssentialAction = withAuth { username =>
protected def withUser(adminRightsRequired: Boolean)(f: User => Request[AnyContent] => Result): EssentialAction = withAuth { username =>
implicit request => {
tableDefs.futureUserByUserName(username) map {
case Some(user) => f(user)(request)
case None => onUnauthorized(request)
case Some(user) =>
if (!adminRightsRequired || user.isAdmin) f(user)(request)
else onUnauthorized(request)
}
}
}
protected def futureWithUser(f: User => Request[AnyContent] => Future[Result])(implicit ec: ExecutionContext): EssentialAction = withAuth { username =>
protected def futureWithUser(adminRightsRequired: Boolean)(f: User => Request[AnyContent] => Future[Result]): EssentialAction = withAuth { username =>
implicit request =>
tableDefs.futureUserByUserName(username) flatMap {
case Some(user) => f(user)(request)
case None => futureOnUnauthorized(request)
case Some(user) =>
if (!adminRightsRequired || user.isAdmin) f(user)(request)
else futureOnUnauthorized(request)
}
}
def futureWithUserWithBodyParser[A](bodyParser: BodyParser[A])(f: User => Request[A] => Future[Result])(implicit ec: ExecutionContext): EssentialAction =
withAuthWithBodyParser(bodyParser) { username =>
implicit request =>
tableDefs.futureUserByUserName(username) flatMap {
case Some(user) => f(user)(request)
case None => futureOnUnauthorized(request)
}
}
}
\ No newline at end of file
......@@ -10,6 +10,8 @@ object Consts {
val correctName : String = "correct"
val correctnessName: String = "correctness"
val excelFileName: String = "excelFile"
val idName : String = "id"
val indexName: String = "index"
......
......@@ -6,7 +6,9 @@ import Consts._
final case class LoginFormValues(username: String, password: String)
final case class RegisterFormValues(username: String, name: String, pw: String, pwRepeat: String)
final case class RegisterFormValues(username: String, pw: String, pwRepeat: String)
final case class NewCollectionFormValues()
object FormMappings {
......@@ -20,10 +22,13 @@ object FormMappings {
val registerValuesForm: Form[RegisterFormValues] = Form(
mapping(
usernameName -> nonEmptyText,
nameName -> nonEmptyText,
pwName -> nonEmptyText,
repeatPwName -> nonEmptyText
)(RegisterFormValues.apply)(RegisterFormValues.unapply)
)
val newLanguageValuesForm: Form[String] = Form(single(nameName -> nonEmptyText))
val newCollectionValuesForm: Form[String] = Form(single(nameName -> nonEmptyText))
}
package model
import better.files._
import org.apache.poi.ss.usermodel.Row.MissingCellPolicy
import org.apache.poi.ss.usermodel.{CellType, Row}
import org.apache.poi.xssf.usermodel.XSSFWorkbook
object Importer {
private val stdFile: File = file"conf/vokabeln.xlsx"
private val cardTypeCellIndex: Int = 0
private val questionCellIndex: Int = 1
private val meaningCellIndex : Int = 2
private def cardTypeFromString(cardTypeStr: String): Either[String, CardType] = cardTypeStr match {
case "Wort" => Right(CardType.Vocable)
case "Text" => Right(CardType.Text)
case "SC" => Right(CardType.SingleChoice)
case "MC" => Right(CardType.MultipleChoice)
case other => Left(other)
}
private def partitionEitherSeq[T, U](a: Seq[Either[T, U]]): (Seq[T], Seq[U]) =
a.foldLeft[(Seq[T], Seq[U])]((Seq[T](), Seq[U]())) { (b, a) =>
a match {
case Left(t) => (b._1 :+ t, b._2);
case Right(u) => (b._1, b._2 :+ u)
}
}
def importFlashcards(langId: Int, collId: Int, file: File = stdFile): (Seq[String], Seq[CompleteFlashcard]) = {
val workbook = new XSSFWorkbook(file.path.toAbsolutePath.toFile)
val sheet = workbook.getSheetAt(workbook.getActiveSheetIndex)
val firstRowWithoutHeaderInex = sheet.getFirstRowNum + 1
// Ignore header row
val readFlashcards = (firstRowWithoutHeaderInex to sheet.getLastRowNum) flatMap { rowIndex =>
Option(sheet.getRow(rowIndex)) match {
case None => None
case Some(row) => Some(readRow(row, langId, collId))
}
}
workbook.close()
partitionEitherSeq(readFlashcards)
}
private def readChoiceRow(row: Row, langId: Int, collId: Int, cardType: CardType, question: String): Either[String, CompleteFlashcard] = {
val cardId = row.getRowNum
val lastCellNum = row.getLastCellNum
val maxCellIndex = if (lastCellNum % 2 == 1) lastCellNum + 1 else lastCellNum
val (failures, answers): (Seq[String], Seq[ChoiceAnswer]) = partitionEitherSeq((meaningCellIndex to maxCellIndex by 2).map { cellIndex =>
readStringCell(row, cellIndex) map { answer =>
val id = (cellIndex - meaningCellIndex) / 2
val correctnessCellStringValue = row.getCell(cellIndex + 1, MissingCellPolicy.CREATE_NULL_AS_BLANK).getStringCellValue
val correctness = if (correctnessCellStringValue.nonEmpty) Correctness.Correct else Correctness.Wrong
ChoiceAnswer(id, cardId, collId, langId, answer, correctness)
}
})
failures.foreach(println)
Right(CompleteFlashcard(
Flashcard(cardId, collId, langId, cardType, question, meaning = None),
choiceAnswers = answers
))
}
private def readTextRow(row: Row, langId: Int, collId: Int, cardType: CardType, question: String): Either[String, CompleteFlashcard] =
readStringCell(row, meaningCellIndex) map { meaning =>
CompleteFlashcard(
Flashcard(row.getRowNum, collId, langId, cardType, question, Some(meaning)),
choiceAnswers = Seq[ChoiceAnswer]()
)
}
private def readRow(row: Row, langId: Int, collId: Int): Either[String, CompleteFlashcard] =
readStringCell(row, cardTypeCellIndex) flatMap { cardTypeString: String =>
cardTypeFromString(cardTypeString) flatMap { cardType: CardType =>
readStringCell(row, questionCellIndex) flatMap { question: String =>
cardType match {
case CardType.Vocable | CardType.Text => readTextRow(row, langId, collId, cardType, question)
case CardType.SingleChoice | CardType.MultipleChoice => readChoiceRow(row, langId, collId, cardType, question)
}