Commit becf4d23 authored by Tom Käsler's avatar Tom Käsler

init commit

parents
.idea/
project/target
target
# ARSnova 3 Backend Prototype
Akka for http
Slick for db
Flyway for migration
name := "ARSnova-prototype"
version := "0.0.1"
scalaVersion := "2.11.8"
libraryDependencies ++= {
val akkaVersion = "2.4.8"
val scalaTestVersion = "3.0.0"
val scalaMockVersion = "3.2.2"
val slickVersion = "3.1.1"
Seq(
"com.typesafe.akka" %% "akka-actor" % akkaVersion,
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
"com.typesafe.akka" %% "akka-http-core" % akkaVersion,
"com.typesafe.akka" %% "akka-http-experimental" % akkaVersion,
"com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaVersion,
"com.typesafe.akka" %% "akka-http-testkit" % akkaVersion,
"com.typesafe.slick" %% "slick" % slickVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion,
"org.slf4j" % "slf4j-nop" % "1.7.21",
"mysql" % "mysql-connector-java" % "6.0.3",
"org.flywaydb" % "flyway-core" % "3.2.1",
"com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test",
"org.scalatest" %% "scalatest" % scalaTestVersion
// "org.scalamock" %% "scalamock-scalatest-support" % scalaMockVersion
)
}
addSbtPlugin("io.spray" % "sbt-revolver" % "0.8.0")
akka {
loglevel = WARNING
}
database = {
url = "jdbc:mysql://127.0.0.1/arsnova_prototype?useSSL=false&serverTimezone=Europe/Berlin"
url = ${?PSQL_URL}
user = "arsnova_prototype"
user = ${?PSQL_USER}
password = "arsnova_prototype"
password = ${?PSQL_PASSWORD}
driver = com.mysql.jdbc.Driver
numThreads = 5
}
http {
interface = "0.0.0.0"
port = 9000
}
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL,
pwd VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
) ENGINE=INNODB;
\ No newline at end of file
CREATE TABLE sessions (
id INT NOT NULL AUTO_INCREMENT,
sessionkey VARCHAR(8) NOT NULL,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
short_title VARCHAR(255) NOT NULL,
PRIMARY KEY(id),
CONSTRAINT session_user_fk FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=INNODB;
\ No newline at end of file
CREATE TABLE questions (
id INT NOT NULL AUTO_INCREMENT,
session_id INT NOT NULL,
subject VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
variant VARCHAR(255) NOT NULL,
format VARCHAR(255) NOT NULL,
backside TEXT,
PRIMARY KEY(id),
CONSTRAINT question_session_fk FOREIGN KEY (session_id) REFERENCES sessions(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=INNODB;
\ No newline at end of file
CREATE TABLE answer_options (
id INT NOT NULL AUTO_INCREMENT,
question_id INT NOT NULL,
correct TINYINT(1) NOT NULL,
content TEXT NOT NULL,
points INT NOT NULL,
PRIMARY KEY(id),
CONSTRAINT possible_answer_question_fk FOREIGN KEY (question_id) REFERENCES questions(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=INNODB;
\ No newline at end of file
CREATE TABLE freetext_answers (
id INT NOT NULL AUTO_INCREMENT,
question_id INT NOT NULL,
session_id INT NOT NULL,
subject TEXT NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY(id),
CONSTRAINT freetext_answer_question_fk FOREIGN KEY (question_id) REFERENCES questions(id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT freetext_answer_session_fk FOREIGN KEY (session_id) REFERENCES sessions(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=INNODB;
\ No newline at end of file
CREATE TABLE choice_answers (
id INT NOT NULL AUTO_INCREMENT,
question_id INT NOT NULL,
session_id INT NOT NULL,
answer_option_id INT NOT NULL,
PRIMARY KEY(id),
CONSTRAINT choice_answer_question_fk FOREIGN KEY (question_id) REFERENCES questions(id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT choice_answer_session_fk FOREIGN KEY (session_id) REFERENCES sessions(id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT choice_answer_answer_option_fk FOREIGN KEY (answer_option_id) REFERENCES answer_options(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=INNODB;
\ No newline at end of file
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
akkaHttp + slick rest api
</body>
</html>
\ No newline at end of file
import akka.actor.ActorSystem
import akka.event.{Logging, LoggingAdapter}
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import utils.{MigrationConfig, Config}
import scala.concurrent.ExecutionContext
object Main extends App with Config with MigrationConfig with Routes with TestData {
private implicit val system = ActorSystem()
protected implicit val executor: ExecutionContext = system.dispatcher
protected val log: LoggingAdapter = Logging(system, getClass)
protected implicit val materializer: ActorMaterializer = ActorMaterializer()
//migrate()
reloadSchema()
//populateDB
Http().bindAndHandle(handler = logRequestResult("log")(routes), interface = httpInterface, port = httpPort)
}
\ No newline at end of file
import akka.http.scaladsl.server.Directives._
import api._
trait Routes extends ApiErrorHandler with UserApi with SessionApi with QuestionApi with FreetextAnswerApi with ChoiceAnswerApi {
val routes =
pathPrefix("api") {
userApi ~
sessionApi ~
questionApi ~
freetextAnswerApi ~
choiceAnswerApi
} ~ path("")(getFromResource("public/index.html"))
}
import models._
import services._
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global
import slick.driver.MySQLDriver.api._
trait TestData extends BaseService {
val setup = DBIO.seq(
usersTable += User(None, "user1", "adfsie324"),
usersTable += User(None, "user2", "320948492304"),
usersTable += User(None, "user3", "iae90898988"),
sessionsTable += Session(None, "12345678", 1, "session1", "s1"),
sessionsTable += Session(None, "11111111", 1, "session2", "s2"),
sessionsTable += Session(None, "87654321", 1, "session3", "s2"),
questionsTable += Freetext(None, 1, "subject1", "First Test Question \\o/", "preparation", "freetext"),
questionsTable += Freetext(None, 1, "subject2", "Second Question. Isn't that nice?", "preparation", "freetext"),
questionsTable += Freetext(None, 1, "subject3", "First lecture question", "lecture", "freetext"),
questionsTable += Flashcard(None, 1, "subject4", "Test Data isn't my thing", "lecture", "flashcard", "this is a backside. I'm making a note here: huge success!"),
questionsTable += ChoiceQuestion(None, 1, "subject5", "Last but not least", "lecture", "mc", Nil),
questionsTable += ChoiceQuestion(None, 1, "subject1", "First Question on second session", "lecture", "mc", Nil),
answerOptionsTable += AnswerOption(None, 5, true, "FirstAnswerOption", 10),
answerOptionsTable += AnswerOption(None, 5, false, "FirstAnswerOption", -10),
answerOptionsTable += AnswerOption(None, 5, false, "FirstAnswerOption", -10),
answerOptionsTable += AnswerOption(None, 5, false, "FirstAnswerOption", -10),
answerOptionsTable += AnswerOption(None, 5, true, "FirstAnswerOption", 10),
answerOptionsTable += AnswerOption(None, 6, false, "FirstAnswerOption", -10),
answerOptionsTable += AnswerOption(None, 6, false, "FirstAnswerOption", -10),
answerOptionsTable += AnswerOption(None, 6, true, "FirstAnswerOption", 10)
)
def populateDB: Unit = {
val setupFuture = db.run(setup)
setupFuture.onComplete {
case Success(dunno) => println("testdata imported")
case Failure(t) => println("An error has occured: " + t.getMessage)
}
}
}
package api
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.ExceptionHandler
trait ApiErrorHandler {
implicit def myExceptionHandler: ExceptionHandler = ExceptionHandler {
case e: NoSuchElementException =>
extractUri { uri =>
complete(HttpResponse(NotFound, entity = s"Invalid id: ${e.getMessage}"))
}
}
}
package api
import services.AnswerService
import scala.concurrent.ExecutionContext.Implicits.global
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import models._
import akka.http.scaladsl.server.Directives._
import spray.json._
trait ChoiceAnswerApi {
import mappings.ChoiceAnswerJsonProtocol._
val choiceAnswerApi = pathPrefix("question") {
pathPrefix(IntNumber) { id =>
pathPrefix("choiceAnswer") {
pathEndOrSingleSlash {
post {
entity(as[ChoiceAnswer]) { answer =>
complete (AnswerService.createChoiceAnswer(answer).map(_.toJson))
}
}
}
}
}
}
}
package api
import services.AnswerService
import scala.concurrent.ExecutionContext.Implicits.global
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import models._
import akka.http.scaladsl.server.Directives._
import spray.json._
trait FreetextAnswerApi {
import mappings.FreetextAnswerJsonProtocol._
//import mappings.ChoiceAnswerJsonProtocol._
val freetextAnswerApi = pathPrefix("question") {
pathPrefix(IntNumber) { id =>
pathPrefix("freetextAnswer") {
pathEndOrSingleSlash {
post {
entity(as[FreetextAnswer]) { answer =>
complete (AnswerService.createFreetextAnswer(answer).map(_.toJson))
}
}
}
}
}
}
}
package api
import services.QuestionService
import scala.concurrent.ExecutionContext.Implicits.global
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import models._
import akka.http.scaladsl.server.Directives._
import spray.json._
trait QuestionApi {
import mappings.QuestionJsonProtocol._
val questionApi = pathPrefix("question") {
pathEndOrSingleSlash {
get {
parameters("sessionid".as[SessionId], "variant".as[String]) { (sessionId, variant) =>
complete(QuestionService.findQuestionsBySessionIdAndVariant(sessionId, variant))
}
parameter("sessionid".as[SessionId]) { sessionId =>
complete {QuestionService.findAllBySessionId(sessionId)}
}
} ~
post {
entity(as[Question]) { question =>
complete (QuestionService.create(question).map(_.toJson))
}
}
} ~
pathPrefix(IntNumber) { id =>
pathEndOrSingleSlash {
get {
complete (QuestionService.getById(id))
} ~
put {
entity(as[Question]) { question =>
complete (QuestionService.update(question, id).map(_.toJson))
}
} ~
delete {
complete (QuestionService.delete(id).map(_.toJson))
}
}
}
}
}
package api
import services.SessionService
import scala.concurrent.ExecutionContext.Implicits.global
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import models._
import akka.http.scaladsl.server.Directives._
import spray.json._
trait SessionApi {
import mappings.SessionJsonProtocol._
val sessionApi = pathPrefix("session") {
pathEndOrSingleSlash {
get {
parameter("user".as[UserId]) { (userId) =>
complete (SessionService.findUserSessions(userId))
}
} ~
post {
entity(as[Session]) { session =>
complete (SessionService.create(session).map(_.toJson))
}
}
} ~
pathPrefix(IntNumber) { sessionId =>
pathEndOrSingleSlash {
get {
complete (SessionService.findById(sessionId))
} ~
put {
entity(as[Session]) { session =>
complete (SessionService.update(session, sessionId).map(_.toJson))
}
} ~
delete {
complete (SessionService.delete(sessionId).map(_.toJson))
}
}
}
}
}
package api
import services.UserService
import scala.concurrent.ExecutionContext.Implicits.global
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import models._
import akka.http.scaladsl.server.Directives._
import spray.json._
trait UserApi {
import mappings.UserJsonProtocol._
val userApi = pathPrefix("") {
pathEndOrSingleSlash {
get {
complete (UserService.findAll)
} ~
post {
entity(as[User]) { user =>
complete (UserService.create(user).map(_.toJson))
}
}
} ~
pathPrefix(IntNumber) { userId =>
pathEndOrSingleSlash {
get {
complete (UserService.findById(userId))
} ~
put {
entity(as[User]) { user =>
complete (UserService.update(user, userId).map(_.toJson))
}
} ~
delete {
complete (UserService.delete(userId).map(_.toJson))
}
}
}
}
}
package mappings
import models.AnswerOption
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
object AnswerOptionJsonProtocol extends DefaultJsonProtocol {
implicit val answerOptionFormat: RootJsonFormat[AnswerOption] = jsonFormat5(AnswerOption)
}
package mappings
import models.ChoiceAnswer
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
object ChoiceAnswerJsonProtocol extends DefaultJsonProtocol {
implicit val choiceAnswerFormat: RootJsonFormat[ChoiceAnswer] = jsonFormat4(ChoiceAnswer)
}
package mappings
import models.FreetextAnswer
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
object FreetextAnswerJsonProtocol extends DefaultJsonProtocol {
implicit val freetextAnswerFormat: RootJsonFormat[FreetextAnswer] = jsonFormat5(FreetextAnswer)
}
package mappings
import models.{ChoiceQuestion, Flashcard, Freetext, Question}
import spray.json._
object QuestionJsonProtocol extends DefaultJsonProtocol {
import AnswerOptionJsonProtocol._
implicit object questionFormat extends RootJsonFormat[Question] {
implicit val choiceQuestionFormat: RootJsonFormat[ChoiceQuestion] = jsonFormat7(ChoiceQuestion)
implicit val freetextFormat: RootJsonFormat[Freetext] = jsonFormat6(Freetext)
implicit val flashcardFormat: RootJsonFormat[Flashcard] = jsonFormat7(Flashcard)
def write(q: Question): JsValue = q match {
case Freetext(_, _, _, _, _, _) => q.asInstanceOf[Freetext].toJson
case Flashcard(_, _, _, _, _, "flashcard", _) => q.asInstanceOf[Flashcard].toJson
case ChoiceQuestion(_, _, _, _, _, _, _) => q.asInstanceOf[ChoiceQuestion].toJson
}
def read(json: JsValue) = {
json.asJsObject.getFields(
"format"
) match {
case Seq(JsString(format)) => format match {
case "flashcard" => json.convertTo[Flashcard]
case "mc" => json.convertTo[ChoiceQuestion]
case "freetext" => json.convertTo[Freetext]
}
}
}
}
}
package mappings
import models.Session
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
object SessionJsonProtocol extends DefaultJsonProtocol {
implicit val sessionFormat: RootJsonFormat[Session] = jsonFormat5(Session)
}
package mappings
import models.User
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
object UserJsonProtocol extends DefaultJsonProtocol {
implicit val userFormat: RootJsonFormat[User] = jsonFormat3(User)
}
package models
case class FreetextAnswer(id: Option[FreetextAnswerId], questionId: QuestionId, sessionId: SessionId, subject: String, text: String)
case class ChoiceAnswer(id: Option[ChoiceAnswerId], questionId: QuestionId, sessionId: SessionId, answerOptionId: AnswerOptionId)
package models
case class AnswerOption(id: Option[AnswerOptionId], questionId: QuestionId, correct: Boolean, text: String, value: Int)
\ No newline at end of file
package models
trait Question {
val id: Option[QuestionId]
val sessionId: SessionId
val subject: String
val content: String
val variant: String
val format: String
}
/*trait ChoiceQuestion {
val answerOptions: Seq[AnswerOption]
}*/
case class Freetext(id: Option[QuestionId], sessionId: SessionId, subject: String, content: String, variant: String, format: String) extends Question
case class Flashcard(id: Option[QuestionId], sessionId: SessionId, subject: String, content: String, variant: String, format: String, backside: String) extends Question
//case class MC(id: Option[QuestionId], sessionId: SessionId, subject: String, content: String, variant: String, format: String, answerOptions: Seq[AnswerOption], hasCorrectAnswer: Boolean) extends Question with ChoiceQuestion
case class ChoiceQuestion(id: Option[QuestionId], sessionId: SessionId, subject: String, content: String, variant: String, format: String, answerOptions: Seq[AnswerOption]) extends Question
package models
case class Session(id: Option[SessionId], key: String, userId: UserId, title: String, shortTitle: String)
package models
case class User(id: Option[UserId], userName: String, password: String)
package models.definitions
import models.{AnswerOption, AnswerOptionId, QuestionId}
import slick.driver.MySQLDriver.api._
class AnswerOptionsTable(tag: Tag) extends Table[AnswerOption](tag, "answer_options") {
def id = column[AnswerOptionId]("id", O.PrimaryKey, O.AutoInc)
def questionId = column[QuestionId]("question_id")
def correct = column[Boolean]("correct")
def text = column[String]("content")
def value = column[Int]("points")
def * = (id.?, questionId, correct, text, value) <> ((AnswerOption.apply _).tupled, AnswerOption.unapply)
def question = foreignKey("answer_option_question_fk", questionId, TableQuery[QuestionsTable])(_.id)
}
\ No newline at end of file
package models.definitions
import models.{ChoiceAnswer, ChoiceAnswerId, QuestionId, SessionId, AnswerOptionId}
import slick.driver.MySQLDriver.api._
class ChoiceAnswersTable(tag: Tag) extends Table[ChoiceAnswer](tag, "choice_answers") {
def id = column[ChoiceAnswerId]("id", O.PrimaryKey, O.AutoInc)
def questionId = column[QuestionId]("question_id")
def sessionId = column[SessionId]("session_id")
def answerOptionId = column[AnswerOptionId]("answer_option_id")
def * = (id.?, questionId, sessionId, answerOptionId) <> ((ChoiceAnswer.apply _).tupled, ChoiceAnswer.unapply)
}
package models.definitions
import models.{FreetextAnswer, FreetextAnswerId, QuestionId, SessionId}
import slick.driver.MySQLDriver.api._
class FreetextAnswersTable(tag: Tag) extends Table[FreetextAnswer](tag, "freetext_answers") {
def id = column[FreetextAnswerId]("id", O.PrimaryKey, O.AutoInc)
def questionId = column[QuestionId]("question_id")
def sessionId = column[SessionId]("session_id")
def subject = column[String]("subject")
def content = column[String]("content")
def * = (id.?, questionId, sessionId, subject, content) <> ((FreetextAnswer.apply _).tupled, FreetextAnswer.unapply)
def session = foreignKey("freetext_answer_session_fk", sessionId, TableQuery[QuestionsTable])(_.id)
def question = foreignKey("freetext_answer_question_fk", questionId, TableQuery[QuestionsTable])(_.id)
}
package models.definitions
import models._
import slick.driver.MySQLDriver.api._
import slick.profile.SqlProfile.ColumnOption.Nullable
class QuestionsTable(tag: Tag) extends Table[Question](tag, "questions"){
def id = column[QuestionId]("id", O.PrimaryKey, O.AutoInc)
def sessionId = column[Long]("session_id")
def subject = column[String]("subject")
def content = column[String]("content")
def variant = column[String]("variant")
def format = column[String]("format")
def backside = column[String]("backside")
//def * = (id.?, sessionId, subject, content, variant, format, backside.?, hasCorrectAnswer.?) <> ((Question.apply _).tupled, Question.unapply)
/*def * = format <> ({t: String => t match {
case "flashcard" => (id.?, sessionId, subject, content, variant, format, backside) <> ((Flashcard.apply _).tupled, Flashcard.unapply)
}
})*/
def * = (id.?, sessionId, subject, content, variant, format, backside.?) <> (
{ t: (Option[QuestionId], SessionId, String, String, String, String, Option[String]) => t match {
case (id, sessionId, subject, content, variant, "flashcard", Some(backside)) => new Flashcard(id, sessionId, subject, content, variant, "flashcard", backside):Question
case (id, sessionId, subject, content, variant, "mc", _) => new ChoiceQuestion(id, sessionId, subject, content, variant, "mc", Nil):Question
case (id, sessionId, subject, content, variant, "freetext", _) => new Freetext(id, sessionId, subject, content, variant, "freetext")
}}, { k: Question => k match {
case Flashcard(id, sessionId, subject, content, variant, format, backside) => Some((id, sessionId, subject, content, variant, "flashcard", Some(backside))): Option[(Option[QuestionId], SessionId, String, String, String, String, Option[String])]
case ChoiceQuestion(id, sessionId, subject, content, variant, format, answerOptions) => Some((id, sessionId, subject, content, variant, "mc", None)): Option[(Option[QuestionId], SessionId, String, String, String, String, Option[String])]
case Freetext(id, sessionId, subject, content, variant, format) => Some((id, sessionId, subject, content, variant, "freetext", None)): Option[(Option[QuestionId], SessionId, String, String, String, String, Option[String])]
}})
def session = foreignKey("question_session_fk", sessionId, TableQuery[SessionsTable])(_.id)
}
package models.definitions
import models.{SessionId, UserId, Session}
import slick.driver.MySQLDriver.api._
class SessionsTable(tag: Tag) extends Table[Session](tag, "sessions"){
def id = column[SessionId]("id", O.PrimaryKey, O.AutoInc)
def key = column[String]("sessionkey")
def userId = column[UserId]("user_id")
def title = column[String]("title")
def shortTitle = column[String]("short_title")
def * = (id.?, key, userId, title, shortTitle) <> ((Session.apply _).tupled, Session.unapply)
def author = foreignKey("session_user_fk", userId, TableQuery[UsersTable])(_.id)
}
package models.definitions
import models.{UserId, User}
import slick.driver.MySQLDriver.api._
class UsersTable(tag: Tag) extends Table[User](tag, "users"){
def id = column[UserId]("id", O.PrimaryKey, O.AutoInc)
def username = column[String]("username")
def password = column[String]("pwd")
def * = (id.?, username, password) <> ((User.apply _).tupled, User.unapply)
}
package object models {
type UserId = Long
type SessionId = Long
type QuestionId = Long
type AnswerOptionId = Long
type FreetextAnswerId = Long
type ChoiceAnswerId = Long
}
package services
import models._
import slick.driver.MySQLDriver.api._
import scala.concurrent.Future
object AnswerOptionService extends BaseService {
def findByQuestionId(questionId: QuestionId): Future[Seq[AnswerOption]] = {
(for {
answerOption <- answerOptionsTable.filter(_.questionId === questionId)