diff --git a/play-scala-isolated-slick-example/app/controllers/HomeController.scala b/play-scala-isolated-slick-example/app/controllers/HomeController.scala deleted file mode 100644 index 9f82b47e3..000000000 --- a/play-scala-isolated-slick-example/app/controllers/HomeController.scala +++ /dev/null @@ -1,21 +0,0 @@ -package controllers - -import javax.inject.{Inject, Singleton} - -import com.example.user.UserDAO -import play.api.mvc._ - -import scala.concurrent.ExecutionContext - -@Singleton -class HomeController @Inject() (userDAO: UserDAO, cc: ControllerComponents) - (implicit ec: ExecutionContext) - extends AbstractController(cc) { - - def index = Action.async { implicit request => - userDAO.all.map { users => - Ok(views.html.index(users)) - } - } - -} diff --git a/play-scala-isolated-slick-example/app/controllers/UserController.scala b/play-scala-isolated-slick-example/app/controllers/UserController.scala new file mode 100644 index 000000000..b83937b32 --- /dev/null +++ b/play-scala-isolated-slick-example/app/controllers/UserController.scala @@ -0,0 +1,140 @@ +package controllers + +import com.example.user.{User, UserDAO} +import models.UserRequest +import play.api.i18n.{Lang, Messages} +import play.api.libs.json.Json +import play.api.mvc._ + +import java.time.Instant +import java.util.UUID +import javax.inject.{Inject, Singleton} +import scala.concurrent.ExecutionContext + +@Singleton +class UserController @Inject() (userDAO: UserDAO, cc: ControllerComponents)( + implicit ec: ExecutionContext +) extends AbstractController(cc) { + /** + * An implicit for default Languages for the Inputs to default to english. + */ + implicit val defaultLanguage: Messages = cc.messagesApi.preferred(Seq(Lang("en"))) + + /** + * GET - Get the List of all the users from the userDao + * @return Html View of Index with all the users + */ + def index: Action[AnyContent] = Action.async { implicit request => + userDAO.all.map { users => + Ok(views.html.index(users)) + } + } + + /** + * GET - Find all Users and return them on a String array ids. + * @return List[String] of all the user ids. + */ + def findAll: Action[AnyContent] = Action.async{implicit request => + userDAO.all.map{ users=> + Ok(Json.toJson(users.map(_.id))) + } + } + + /** + * GET - Gets the Create page loading to create a new User + * @return The Html Page for the Create page + */ + def create: Action[AnyContent] = Action { + val emptyForm = UserRequest.form + Ok(views.html.create(emptyForm)) + } + + + /** + * POST - Create a new user from the UserRequest.form validated + * @return Redirect into the index view to see the new user + */ + def save: Action[AnyContent] = Action { implicit request => + UserRequest.form + .bindFromRequest() + .fold( + formWithErrors => BadRequest(views.html.create(formWithErrors)), + formData => { + userDAO.create( + User( + UUID.randomUUID().toString, + formData.email, + createdAt = Instant.now(), + updatedAt = Option(Instant.now()) + ) + ) + Redirect(routes.UserController.index) + } + ) + } + + /** + * GET - Opens the Edit Page for the new user with the form filled from userDAO.lookup + * @param id The Id Parameter of the edited user. + * @return The Html page of the edit filled with the user email to be edited. + */ + def edit(id: String): Action[AnyContent] = Action.async { implicit request => + userDAO.lookup(id).map { userData => + userData.fold( + Redirect(routes.UserController.index) + ) { user => + val filledForm = UserRequest.form + .fill(UserRequest(id = Option(user.id), email = user.email)) + Ok(views.html.update(filledForm)) + } + } + } + + /** + * POST - Update the user via the validated form to the id requested if that exists. + * @param id The id parameter of the user to edit + * @return Redirect to html page of index with the updated user on it. + */ + def update(id: String): Action[AnyContent] = Action.async { + implicit request => + userDAO.lookup(id).map { userData => + userData.fold( + Redirect(routes.UserController.index) + ) { user => + UserRequest.form + .bindFromRequest() + .fold( + formWithErrors => BadRequest(views.html.create(formWithErrors)), + formData => { + userDAO.update( + User( + id, + formData.email, + user.createdAt, + updatedAt = Option(Instant.now()) + ) + ) + Redirect(routes.UserController.index) + } + ) + } + } + } + + /** + * GET - Delete a user from the database based on id - Done GET due to href link + * @param id The user to delete + * @return Redirects to index page where the new user doesnt exist. + */ + def delete(id: String): Action[AnyContent] = Action.async { + implicit unused => + userDAO.lookup(id).map { userData => + userData.fold( + Redirect(routes.UserController.index) + ) { _ => + userDAO.delete(id) + Redirect(routes.UserController.index) + } + } + } +} diff --git a/play-scala-isolated-slick-example/app/models/UserRequest.scala b/play-scala-isolated-slick-example/app/models/UserRequest.scala new file mode 100644 index 000000000..d75b966fe --- /dev/null +++ b/play-scala-isolated-slick-example/app/models/UserRequest.scala @@ -0,0 +1,23 @@ +package models + +import play.api.data.Forms._ +import play.api.data.Form + +/** + * A UserRequest model to get the form to update/create users. + * @param id The Optional User Id to counter add/edit actions + * @param email The Email to be registered/edited + */ +case class UserRequest(id: Option[String], email: String) + +/** + * The Companion object of the form + */ +object UserRequest { + val form: Form[UserRequest] = Form( + mapping( + "id" -> optional(nonEmptyText), + "email" -> email + )(UserRequest.apply)(UserRequest.unapply) + ) +} \ No newline at end of file diff --git a/play-scala-isolated-slick-example/app/views/create.scala.html b/play-scala-isolated-slick-example/app/views/create.scala.html new file mode 100644 index 000000000..721c8a38e --- /dev/null +++ b/play-scala-isolated-slick-example/app/views/create.scala.html @@ -0,0 +1,13 @@ +@(userForm: Form[UserRequest])(implicit messages: Messages) + + @import views.html.helper._ + + @main("Create User") { + +

Create a new User

+ + @form(action = routes.UserController.save) { + @inputText(userForm("email")) + + } + } \ No newline at end of file diff --git a/play-scala-isolated-slick-example/app/views/index.scala.html b/play-scala-isolated-slick-example/app/views/index.scala.html index 6a5dbe123..8171845a1 100644 --- a/play-scala-isolated-slick-example/app/views/index.scala.html +++ b/play-scala-isolated-slick-example/app/views/index.scala.html @@ -1,22 +1,32 @@ @(users: Seq[User]) -@main("Title Page") { -

Users

- - - - - - - - - @for(user <- users){ + @import helper._ + @main("Title Page") { +

Users

+ Create User +
+
+
+
IdEmailCreated AtUpdated At
- - - - + + + + + - } -
@user.id@user.email@user.createdAt@user.updatedAtIdEmailCreated AtUpdated AtActions
-} + @for(user <- users) { + + @user.id + @user.email + @user.createdAt + @user.updatedAt + + Edit + Delete + + + + } + + } diff --git a/play-scala-isolated-slick-example/app/views/main.scala.html b/play-scala-isolated-slick-example/app/views/main.scala.html index aff0eff1d..c4137a039 100644 --- a/play-scala-isolated-slick-example/app/views/main.scala.html +++ b/play-scala-isolated-slick-example/app/views/main.scala.html @@ -7,9 +7,11 @@ @title + + - @content + @content diff --git a/play-scala-isolated-slick-example/app/views/update.scala.html b/play-scala-isolated-slick-example/app/views/update.scala.html new file mode 100644 index 000000000..dfb258ac4 --- /dev/null +++ b/play-scala-isolated-slick-example/app/views/update.scala.html @@ -0,0 +1,16 @@ +@(userForm: Form[UserRequest])(implicit messages: Messages) + +@import views.html.helper._ + +@main("Update User") { + +

Update User

+ + @form(routes.UserController.update(userForm("id").value.get)) { + + @inputText(userForm("email")) + + + + } +} diff --git a/play-scala-isolated-slick-example/conf/routes b/play-scala-isolated-slick-example/conf/routes index f59f0a3b1..6705c5147 100644 --- a/play-scala-isolated-slick-example/conf/routes +++ b/play-scala-isolated-slick-example/conf/routes @@ -2,8 +2,18 @@ # This file defines all application routes (Higher priority routes first) # ~~~~ -# Home page -GET / controllers.HomeController.index +# User Routes + +GET / controllers.UserController.index +GET /users/all controllers.UserController.findAll + +GET /users/new controllers.UserController.create +POST /users controllers.UserController.save + +GET /users/:id/edit controllers.UserController.edit(id: String) +POST /users/:id controllers.UserController.update(id: String) + +GET /users/:id/delete controllers.UserController.delete(id: String) # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/play-scala-isolated-slick-example/modules/slick/src/main/resources/application.conf b/play-scala-isolated-slick-example/modules/slick/src/main/resources/application.conf index 43eb52cc0..4dcc3057d 100644 --- a/play-scala-isolated-slick-example/modules/slick/src/main/resources/application.conf +++ b/play-scala-isolated-slick-example/modules/slick/src/main/resources/application.conf @@ -1,4 +1,7 @@ +//Disabling to allow posts from forms. +play.filters.disabled += play.filters.csrf.CSRFFilter + myapp = { database = { driver = org.h2.Driver diff --git a/play-scala-isolated-slick-example/public/stylesheets/main.css b/play-scala-isolated-slick-example/public/stylesheets/main.css index e69de29bb..e0d78b326 100644 --- a/play-scala-isolated-slick-example/public/stylesheets/main.css +++ b/play-scala-isolated-slick-example/public/stylesheets/main.css @@ -0,0 +1,77 @@ +body { + font-family: Arial, sans-serif; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + border: 1px solid #ddd; + text-align: left; + padding: 8px; +} + +th { + background-color: #f2f2f2; +} + +tr:nth-child(even) { + background-color: #f9f9f9; +} + +.add-button, .edit-button, .delete-button { + text-decoration: none; + padding: 5px 10px; + border-radius: 5px; + color: white; + margin-right: 5px; +} + +.add-button { + background-color: #4CAF50; /* Green */ +} + +.edit-button { + background-color: #2196F3; /* Blue */ +} + +.delete-button { + background-color: #f44336; /* Red */ +} + +input[type="text"], input[type="email"], input[type="password"] { + width: calc(100% - 20px); + padding: 10px; + margin: 8px 0; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +button[type="submit"] { + width: 100%; + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + border-radius: 4px; + cursor: pointer; +} + +button[type="submit"]:hover { + background-color: #45a049; +} + +form { + margin: auto; + width: 50%; + padding: 10px; +} + +label[for="email"] { + display: none; +} diff --git a/play-scala-isolated-slick-example/scripts/test-sbt b/play-scala-isolated-slick-example/scripts/test-sbt index fe70d2df8..2b63ba001 100755 --- a/play-scala-isolated-slick-example/scripts/test-sbt +++ b/play-scala-isolated-slick-example/scripts/test-sbt @@ -4,4 +4,4 @@ echo "+----------------------------+" echo "| Executing tests using sbt |" echo "+----------------------------+" rm -f test.mv.db test.trace.db -sbt ++$MATRIX_SCALA clean flyway/flywayMigrate slickCodegen test +sbt ++$MATRIX_SCALA clean reload flyway/flywayMigrate slickCodegen test diff --git a/play-scala-isolated-slick-example/test/controller/FunctionalSpec.scala b/play-scala-isolated-slick-example/test/controller/FunctionalSpec.scala index 9e1b89f49..27c21b09c 100644 --- a/play-scala-isolated-slick-example/test/controller/FunctionalSpec.scala +++ b/play-scala-isolated-slick-example/test/controller/FunctionalSpec.scala @@ -1,6 +1,8 @@ package controller +import controllers.routes import org.scalatestplus.play.{BaseOneAppPerSuite, PlaySpec} +import play.api.libs.json.{JsValue, Json} import play.api.test.FakeRequest import play.api.test.Helpers._ @@ -10,12 +12,64 @@ import play.api.test.Helpers._ */ class FunctionalSpec extends PlaySpec with BaseOneAppPerSuite with MyApplicationFactory { - "HomeController" should { - + "UserController" should { "work with in memory h2 database" in { - val future = route(app, FakeRequest(GET, "/")).get - contentAsString(future) must include("myuser@example.com") + val futureGetIndex = route(app, FakeRequest(GET, "/")).get + contentAsString(futureGetIndex) must include("myuser@example.com") } - } + "adding a new email will work" in { + val email = "myuser1@gmail.com" + + val futureCreateUser = route(app,FakeRequest(POST, "/users") + .withFormUrlEncodedBody("email" -> email)).get + status(futureCreateUser) mustBe SEE_OTHER + redirectLocation(futureCreateUser) mustBe Some(routes.UserController.index.url) + + val futureGetIndex = route(app, FakeRequest(GET, "/")).get + contentAsString(futureGetIndex) must include(email) + } + + "editing an email will work" in { + val email = "myuser2@gmail.com" + + val futureGetAllUserIds = route(app,FakeRequest(GET,"/users/all")).get + val bodyAsJson: JsValue = Json.parse(contentAsString(futureGetAllUserIds)) + val listOfIds: List[String] = bodyAsJson.as[List[String]] + + val futureUpdateUser = route(app,FakeRequest(POST, s"/users/${listOfIds.head}") + .withFormUrlEncodedBody("email" -> email)).get + + status(futureUpdateUser) mustBe SEE_OTHER + redirectLocation(futureUpdateUser) mustBe Some(routes.UserController.index.url) + + val futureIndex = route(app, FakeRequest(GET, "/")).get + contentAsString(futureIndex) must include(email) + } + + "deleting all users will work" in { + val email = "myuser3@example.com" + + val futureCreateUser = route(app, FakeRequest(POST, "/users") + .withFormUrlEncodedBody("email" -> email)).get + status(futureCreateUser) mustBe SEE_OTHER + redirectLocation(futureCreateUser) mustBe Some(routes.UserController.index.url) + + val futureInitialIndex = route(app, FakeRequest(GET, "/")).get + contentAsString(futureInitialIndex) must include(email) + + val futureGetAllUserIds = route(app, FakeRequest(GET, "/users/all")).get + val bodyAsJson: JsValue = Json.parse(contentAsString(futureGetAllUserIds)) + val listOfIds: List[String] = bodyAsJson.as[List[String]] + + listOfIds.foreach { eachId => + val futureDeleteUser = route(app, FakeRequest(GET, s"/users/$eachId/delete")).get + status(futureDeleteUser) mustBe SEE_OTHER + redirectLocation(futureDeleteUser) mustBe Some(routes.UserController.index.url) + } + + val futureUpdatedIndex = route(app, FakeRequest(GET, "/")).get + contentAsString(futureUpdatedIndex) mustNot include(email) + } + } } diff --git a/play-scala-isolated-slick-example/test/controller/MyApplicationFactory.scala b/play-scala-isolated-slick-example/test/controller/MyApplicationFactory.scala index cd1fcb212..2283fe8df 100644 --- a/play-scala-isolated-slick-example/test/controller/MyApplicationFactory.scala +++ b/play-scala-isolated-slick-example/test/controller/MyApplicationFactory.scala @@ -1,7 +1,5 @@ package controller -import java.util.Properties - import com.google.inject.Inject import org.flywaydb.core.Flyway import org.flywaydb.core.internal.jdbc.DriverDataSource