From 94ce9da22455be5f3ef5e3d0f94ab9bed885b3e0 Mon Sep 17 00:00:00 2001 From: rexim Date: Sat, 18 Apr 2015 15:26:32 +0600 Subject: [PATCH 01/14] Added Maintainer and StagedQuote entities (#100) --- scalajvm/app/models/data/Maintainer.scala | 10 ++++++++++ scalajvm/app/models/data/StagedQuote.scala | 13 +++++++++++++ scalajvm/conf/evolutions/default/8.sql | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 scalajvm/app/models/data/Maintainer.scala create mode 100644 scalajvm/app/models/data/StagedQuote.scala create mode 100644 scalajvm/conf/evolutions/default/8.sql diff --git a/scalajvm/app/models/data/Maintainer.scala b/scalajvm/app/models/data/Maintainer.scala new file mode 100644 index 0000000..a69da78 --- /dev/null +++ b/scalajvm/app/models/data/Maintainer.scala @@ -0,0 +1,10 @@ +package models.data + +import scalikejdbc._ + +case class Maintainer(id: Long, name: String, email: String) +object Maintainer extends SQLSyntaxSupport[Maintainer] { + override val tableName = "maintainer" + def apply(rs: WrappedResultSet): Maintainer = + new Maintainer(rs.long("id"), rs.string("name"), rs.string("email")) +} diff --git a/scalajvm/app/models/data/StagedQuote.scala b/scalajvm/app/models/data/StagedQuote.scala new file mode 100644 index 0000000..3de1711 --- /dev/null +++ b/scalajvm/app/models/data/StagedQuote.scala @@ -0,0 +1,13 @@ +package models.data + +import org.joda.time.DateTime +import scalikejdbc._ + +class StagedQuote(id: Long, token: String, content: String, time: DateTime, stagerIp: String) +object StagedQuote extends SQLSyntaxSupport[StagedQuote] { + override val tableName = "staged_quote" + def apply(rs: WrappedResultSet) = + new StagedQuote(rs.long("id"), rs.string("token"), + rs.string("content"), rs.jodaDateTime("time"), + rs.string("stager_ip")) +} \ No newline at end of file diff --git a/scalajvm/conf/evolutions/default/8.sql b/scalajvm/conf/evolutions/default/8.sql new file mode 100644 index 0000000..884c3da --- /dev/null +++ b/scalajvm/conf/evolutions/default/8.sql @@ -0,0 +1,18 @@ +# --- !Ups +create table if not exists staged_quote ( + id serial primary key, + token varchar(32) not null default encode(gen_random_bytes(16), 'hex'), + content varchar not null, + time timestamp not null, + stager_ip varchar not null +); + +create table if not exists maintainer ( + id serial primary key, + name varchar not null, + email varchar not null +) + +# --- !Downs +drop table if exists maintainer; +drop table if exists staged_quote; From e5beb25b368738e68d8a7981680ffa0176206247 Mon Sep 17 00:00:00 2001 From: rexim Date: Sat, 18 Apr 2015 15:56:57 +0600 Subject: [PATCH 02/14] Retrieve a staged quote and fill the form with it (#100) --- scalajvm/app/controllers/SuggestedQuotes.scala | 9 ++++++--- scalajvm/app/models/data/StagedQuote.scala | 2 +- .../app/models/queries/StagedQuoteQueries.scala | 13 +++++++++++++ scalajvm/conf/routes | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 scalajvm/app/models/queries/StagedQuoteQueries.scala diff --git a/scalajvm/app/controllers/SuggestedQuotes.scala b/scalajvm/app/controllers/SuggestedQuotes.scala index f0ade5c..f52cc76 100644 --- a/scalajvm/app/controllers/SuggestedQuotes.scala +++ b/scalajvm/app/controllers/SuggestedQuotes.scala @@ -2,7 +2,8 @@ package controllers import helpers.{ActionWithTx, Notifications, ReCaptcha} import models.forms.SuggestQuoteForm -import models.queries.{ApproverQueries, SuggestedQuoteQueries} +import models.queries._ +import models.forms._ import play.api.data.Forms._ import play.api.data._ import play.api.mvc._ @@ -21,8 +22,10 @@ object SuggestedQuotes extends Controller { Ok(json).as("application/json") } - def newQuote() = Action { request => - Ok(views.html.newQuote(quoteForm)) + def newQuote(stagedQuoteToken: String) = ActionWithTx { request => + implicit val db = request.dbSession + val content = StagedQuoteQueries().getStagedQuoteByToken(stagedQuoteToken).map(_.content).getOrElse("") + Ok(views.html.newQuote(quoteForm.fill(SuggestQuoteForm(content, "")))) } def addQuote() = ActionWithTx { implicit request => diff --git a/scalajvm/app/models/data/StagedQuote.scala b/scalajvm/app/models/data/StagedQuote.scala index 3de1711..0f36626 100644 --- a/scalajvm/app/models/data/StagedQuote.scala +++ b/scalajvm/app/models/data/StagedQuote.scala @@ -3,7 +3,7 @@ package models.data import org.joda.time.DateTime import scalikejdbc._ -class StagedQuote(id: Long, token: String, content: String, time: DateTime, stagerIp: String) +case class StagedQuote(id: Long, token: String, content: String, time: DateTime, stagerIp: String) object StagedQuote extends SQLSyntaxSupport[StagedQuote] { override val tableName = "staged_quote" def apply(rs: WrappedResultSet) = diff --git a/scalajvm/app/models/queries/StagedQuoteQueries.scala b/scalajvm/app/models/queries/StagedQuoteQueries.scala new file mode 100644 index 0000000..65f45d5 --- /dev/null +++ b/scalajvm/app/models/queries/StagedQuoteQueries.scala @@ -0,0 +1,13 @@ +package models.queries + +import models.data.StagedQuote +import scalikejdbc._ + +case class StagedQuoteQueries(implicit session: DBSession) { + def getStagedQuoteByToken(token: String): Option[StagedQuote] = { + val sq = StagedQuote.syntax("sq") + withSQL { + select(sq.*).from(StagedQuote as sq).where.eq(sq.token, token) + }.map(rs => StagedQuote(rs)).first().apply() + } +} diff --git a/scalajvm/conf/routes b/scalajvm/conf/routes index 7306232..7396983 100644 --- a/scalajvm/conf/routes +++ b/scalajvm/conf/routes @@ -4,7 +4,7 @@ # Main routes GET / controllers.Quotes.list(page: Int ?= 0, order: models.queries.QuoteOrdering.Value ?= models.queries.QuoteOrdering.Time, filter: models.queries.QuoteFilter.Value ?= models.queries.QuoteFilter.None) -GET /quote/new controllers.SuggestedQuotes.newQuote +GET /quote/new controllers.SuggestedQuotes.newQuote(stagedQuoteToken: String ?= "") POST /quote/new controllers.SuggestedQuotes.addQuote GET /quote/$id<[0-9]+> controllers.Quotes.quote(id: Long) POST /quote/$id<[0-9]+>/upvote controllers.Voting.vote(id: Long, up: Boolean = true) From 21ddfb98940afe6b4f67afef51bc2c28ad9fb312 Mon Sep 17 00:00:00 2001 From: rexim Date: Sat, 18 Apr 2015 16:34:48 +0600 Subject: [PATCH 03/14] Optional Stager IP (#100) --- scalajvm/app/models/data/StagedQuote.scala | 4 ++-- scalajvm/conf/evolutions/default/8.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scalajvm/app/models/data/StagedQuote.scala b/scalajvm/app/models/data/StagedQuote.scala index 0f36626..356f4f0 100644 --- a/scalajvm/app/models/data/StagedQuote.scala +++ b/scalajvm/app/models/data/StagedQuote.scala @@ -3,11 +3,11 @@ package models.data import org.joda.time.DateTime import scalikejdbc._ -case class StagedQuote(id: Long, token: String, content: String, time: DateTime, stagerIp: String) +case class StagedQuote(id: Long, token: String, content: String, time: DateTime, stagerIp: Option[String]) object StagedQuote extends SQLSyntaxSupport[StagedQuote] { override val tableName = "staged_quote" def apply(rs: WrappedResultSet) = new StagedQuote(rs.long("id"), rs.string("token"), rs.string("content"), rs.jodaDateTime("time"), - rs.string("stager_ip")) + rs.stringOpt("stager_ip")) } \ No newline at end of file diff --git a/scalajvm/conf/evolutions/default/8.sql b/scalajvm/conf/evolutions/default/8.sql index 884c3da..13d06f8 100644 --- a/scalajvm/conf/evolutions/default/8.sql +++ b/scalajvm/conf/evolutions/default/8.sql @@ -4,14 +4,14 @@ create table if not exists staged_quote ( token varchar(32) not null default encode(gen_random_bytes(16), 'hex'), content varchar not null, time timestamp not null, - stager_ip varchar not null + stager_ip varchar ); create table if not exists maintainer ( id serial primary key, name varchar not null, email varchar not null -) +); # --- !Downs drop table if exists maintainer; From b1a962933350a45b8101dc1bb1615b01a5dfae42 Mon Sep 17 00:00:00 2001 From: rexim Date: Sat, 18 Apr 2015 17:02:21 +0600 Subject: [PATCH 04/14] Implement quote staging with a POST request (#100) --- .../loglist/dto/StagedQuoteDTO.scala | 3 +++ .../app/controllers/SuggestedQuotes.scala | 2 +- scalajvm/app/controllers/api/Quotes.scala | 24 ++++++++++++++++--- scalajvm/app/helpers/ActionWithTx.scala | 2 -- .../models/queries/StagedQuoteQueries.scala | 15 ++++++++++++ scalajvm/conf/routes | 1 + 6 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 scala/src/main/scala/ru/org/codingteam/loglist/dto/StagedQuoteDTO.scala diff --git a/scala/src/main/scala/ru/org/codingteam/loglist/dto/StagedQuoteDTO.scala b/scala/src/main/scala/ru/org/codingteam/loglist/dto/StagedQuoteDTO.scala new file mode 100644 index 0000000..d01c0a9 --- /dev/null +++ b/scala/src/main/scala/ru/org/codingteam/loglist/dto/StagedQuoteDTO.scala @@ -0,0 +1,3 @@ +package ru.org.codingteam.loglist.dto + +case class StagedQuoteDTO(token: String, content: String, time: Long) diff --git a/scalajvm/app/controllers/SuggestedQuotes.scala b/scalajvm/app/controllers/SuggestedQuotes.scala index f52cc76..bff1d5f 100644 --- a/scalajvm/app/controllers/SuggestedQuotes.scala +++ b/scalajvm/app/controllers/SuggestedQuotes.scala @@ -23,7 +23,7 @@ object SuggestedQuotes extends Controller { } def newQuote(stagedQuoteToken: String) = ActionWithTx { request => - implicit val db = request.dbSession + import request.dbSession val content = StagedQuoteQueries().getStagedQuoteByToken(stagedQuoteToken).map(_.content).getOrElse("") Ok(views.html.newQuote(quoteForm.fill(SuggestQuoteForm(content, "")))) } diff --git a/scalajvm/app/controllers/api/Quotes.scala b/scalajvm/app/controllers/api/Quotes.scala index e3807c0..c55f828 100644 --- a/scalajvm/app/controllers/api/Quotes.scala +++ b/scalajvm/app/controllers/api/Quotes.scala @@ -1,12 +1,13 @@ package controllers.api import helpers.ActionWithTx -import models.queries.QuoteQueries -import models.data.Quote +import models.queries._ +import models.data._ import play.api.mvc._ -import ru.org.codingteam.loglist.dto.QuoteDTO +import ru.org.codingteam.loglist.dto.{StagedQuoteDTO, QuoteDTO} object Quotes extends Controller { + def getQuote(id: Long) = ActionWithTx { request => import request.dbSession prepareResponse(QuoteQueries().getQuoteById(id)) @@ -17,6 +18,20 @@ object Quotes extends Controller { prepareResponse(QuoteQueries().getRandomQuote) } + def stageQuote = ActionWithTx { request => + import request.dbSession + request.body.asText match { + case Some(content) => { + val id = StagedQuoteQueries().insertStagedQuote(content, Some(request.remoteAddress)) + StagedQuoteQueries().getStagedQuoteById(id) match { + case Some(stagedQuote) => Ok(upickle.write(buildStagedQuoteDto(stagedQuote))).as("application/json; charset=utf-8") + case None => InternalServerError("The quote was not staged properly").as("text/plain") + } + } + case None => BadRequest("The request body was not found!").as("text/plain") + } + } + private def prepareResponse(probablyQuote: Option[Quote]) = probablyQuote.map(buildQuoteDto) match { case Some(quoteDTO) => Ok(upickle.write(quoteDTO)).as("application/json; charset=utf-8") @@ -25,4 +40,7 @@ object Quotes extends Controller { private def buildQuoteDto(quote: Quote): QuoteDTO = QuoteDTO(quote.id, quote.time.getMillis, quote.content.getOrElse(""), quote.rating) + + private def buildStagedQuoteDto(stagedQuote: StagedQuote): StagedQuoteDTO = + StagedQuoteDTO(stagedQuote.token, stagedQuote.content, stagedQuote.time.getMillis) } diff --git a/scalajvm/app/helpers/ActionWithTx.scala b/scalajvm/app/helpers/ActionWithTx.scala index bd2ae50..db8c7ea 100644 --- a/scalajvm/app/helpers/ActionWithTx.scala +++ b/scalajvm/app/helpers/ActionWithTx.scala @@ -14,5 +14,3 @@ object ActionWithTx extends ActionBuilder[RequestWithSession] { } } } - - diff --git a/scalajvm/app/models/queries/StagedQuoteQueries.scala b/scalajvm/app/models/queries/StagedQuoteQueries.scala index 65f45d5..2bc063e 100644 --- a/scalajvm/app/models/queries/StagedQuoteQueries.scala +++ b/scalajvm/app/models/queries/StagedQuoteQueries.scala @@ -1,6 +1,7 @@ package models.queries import models.data.StagedQuote +import org.joda.time.DateTime import scalikejdbc._ case class StagedQuoteQueries(implicit session: DBSession) { @@ -10,4 +11,18 @@ case class StagedQuoteQueries(implicit session: DBSession) { select(sq.*).from(StagedQuote as sq).where.eq(sq.token, token) }.map(rs => StagedQuote(rs)).first().apply() } + + def getStagedQuoteById(id: Long): Option[StagedQuote] = { + val sq = StagedQuote.syntax("sq") + withSQL { + select(sq.*).from(StagedQuote as sq).where.eq(sq.id, id) + }.map(rs => StagedQuote(rs)).first().apply() + } + + def insertStagedQuote(content: String, stagerIp: Option[String]) = { + val sq = StagedQuote.column + withSQL { + insert.into(StagedQuote).columns(sq.time, sq.content, sq.stagerIp).values(DateTime.now(), content, stagerIp) + }.updateAndReturnGeneratedKey().apply() + } } diff --git a/scalajvm/conf/routes b/scalajvm/conf/routes index 7396983..13858c5 100644 --- a/scalajvm/conf/routes +++ b/scalajvm/conf/routes @@ -19,6 +19,7 @@ GET /quote/count/suggested controllers.SuggestedQuotes.cou # API routes GET /api/quote/$id<[0-9]+> controllers.api.Quotes.getQuote(id: Long) GET /api/quote/random controllers.api.Quotes.getRandomQuote() +POST /api/quote/stage controllers.api.Quotes.stageQuote # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file) From 7a65bf3e0849f2572381958af4f05e2e8434ee4b Mon Sep 17 00:00:00 2001 From: rexim Date: Sun, 19 Apr 2015 18:11:16 +0600 Subject: [PATCH 05/14] Add DisabledAction model (#100) --- scalajvm/app/models/data/DisabledAction.scala | 10 ++++++++++ .../app/models/queries/DisabledActionQueries.scala | 7 +++++++ scalajvm/conf/evolutions/default/8.sql | 5 +++++ 3 files changed, 22 insertions(+) create mode 100644 scalajvm/app/models/data/DisabledAction.scala create mode 100644 scalajvm/app/models/queries/DisabledActionQueries.scala diff --git a/scalajvm/app/models/data/DisabledAction.scala b/scalajvm/app/models/data/DisabledAction.scala new file mode 100644 index 0000000..9243534 --- /dev/null +++ b/scalajvm/app/models/data/DisabledAction.scala @@ -0,0 +1,10 @@ +package models.data + +import scalikejdbc._ + +case class DisabledAction(name: String) +object DisabledAction extends SQLSyntaxSupport[DisabledAction] { + override val tableName = "disabled_action" + def apply(rs: WrappedResultSet): DisabledAction = + new DisabledAction(rs.string("name")) +} diff --git a/scalajvm/app/models/queries/DisabledActionQueries.scala b/scalajvm/app/models/queries/DisabledActionQueries.scala new file mode 100644 index 0000000..7f3eb8f --- /dev/null +++ b/scalajvm/app/models/queries/DisabledActionQueries.scala @@ -0,0 +1,7 @@ +package models.queries + +import scalikejdbc._ + +case class DisabledActionQueries(implicit session: DBSession) { + +} diff --git a/scalajvm/conf/evolutions/default/8.sql b/scalajvm/conf/evolutions/default/8.sql index 13d06f8..09470a6 100644 --- a/scalajvm/conf/evolutions/default/8.sql +++ b/scalajvm/conf/evolutions/default/8.sql @@ -13,6 +13,11 @@ create table if not exists maintainer ( email varchar not null ); +create table if not exists disabled_action ( + name varchar not null unique +); + # --- !Downs +drop table if exists disabled_action; drop table if exists maintainer; drop table if exists staged_quote; From 4e03e8561a047fa83aaeec77a311a3325bd77426 Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 20 Apr 2015 01:18:59 +0600 Subject: [PATCH 06/14] Staging action is disableable (#100) --- scalajvm/app/controllers/api/Quotes.scala | 4 +- .../app/helpers/DisableableActionWithTx.scala | 17 +++++ .../queries/DisabledActionQueries.scala | 21 +++++++ scalajvm/test/ApproverModelSpec.scala | 5 +- scalajvm/test/QuoteStagingDisableSpec.scala | 63 +++++++++++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 scalajvm/app/helpers/DisableableActionWithTx.scala create mode 100644 scalajvm/test/QuoteStagingDisableSpec.scala diff --git a/scalajvm/app/controllers/api/Quotes.scala b/scalajvm/app/controllers/api/Quotes.scala index c55f828..c9ad87a 100644 --- a/scalajvm/app/controllers/api/Quotes.scala +++ b/scalajvm/app/controllers/api/Quotes.scala @@ -1,6 +1,6 @@ package controllers.api -import helpers.ActionWithTx +import helpers._ import models.queries._ import models.data._ import play.api.mvc._ @@ -18,7 +18,7 @@ object Quotes extends Controller { prepareResponse(QuoteQueries().getRandomQuote) } - def stageQuote = ActionWithTx { request => + def stageQuote = DisableableActionWithTx("stage", "Staging is disabled") { request => import request.dbSession request.body.asText match { case Some(content) => { diff --git a/scalajvm/app/helpers/DisableableActionWithTx.scala b/scalajvm/app/helpers/DisableableActionWithTx.scala new file mode 100644 index 0000000..24166b9 --- /dev/null +++ b/scalajvm/app/helpers/DisableableActionWithTx.scala @@ -0,0 +1,17 @@ +package helpers + +import models.queries.DisabledActionQueries +import play.api.mvc._ +import scalikejdbc._ + +import scala.concurrent._ +import ExecutionContext.Implicits.global + +case class DisableableActionWithTx(name: String, message: String) extends ActionBuilder[RequestWithSession] with Results { + override def invokeBlock[A](request: Request[A], block: RequestWithSession[A] => Future[Result]): Future[Result] = { + DB localTx { implicit session => + if (DisabledActionQueries().isActionDisabled(name)) Future(ServiceUnavailable(message).as("text/plain")) + else block(new RequestWithSession[A](request)) + } + } +} diff --git a/scalajvm/app/models/queries/DisabledActionQueries.scala b/scalajvm/app/models/queries/DisabledActionQueries.scala index 7f3eb8f..2e4272e 100644 --- a/scalajvm/app/models/queries/DisabledActionQueries.scala +++ b/scalajvm/app/models/queries/DisabledActionQueries.scala @@ -1,7 +1,28 @@ package models.queries +import models.data.DisabledAction + import scalikejdbc._ case class DisabledActionQueries(implicit session: DBSession) { + def isActionDisabled(name: String): Boolean = { + val da = DisabledAction.syntax("da") + withSQL { + select(da.*).from(DisabledAction as da).where.eq(da.name, name) + }.map(rs => DisabledAction(rs)).first().apply().isDefined + } + + def disableAction(name: String): Unit = { + val da = DisabledAction.column + if (!isActionDisabled(name)) withSQL { + insert.into(DisabledAction).columns(da.name).values(name) + }.update().apply() + } + def enableAction(name: String): Unit = { + val da = DisabledAction.column + if (isActionDisabled(name)) withSQL { + delete.from(DisabledAction).where.eq(da.name, name) + }.update().apply() + } } diff --git a/scalajvm/test/ApproverModelSpec.scala b/scalajvm/test/ApproverModelSpec.scala index ca4642f..0ca3f29 100644 --- a/scalajvm/test/ApproverModelSpec.scala +++ b/scalajvm/test/ApproverModelSpec.scala @@ -5,6 +5,8 @@ import play.api.test.Helpers._ import scalikejdbc._ +import models.data.Approver + class ApproverModelSpec extends Specification { val approvers = List( ("rexim", "rexim@loglist.net"), @@ -15,13 +17,14 @@ class ApproverModelSpec extends Specification { "be able to add new approvers and return them back" in { running(FakeApplication()) { DB localTx { implicit session => + withSQL { deleteFrom(Approver) }.update().apply() + for ((name, email) <- approvers) { models.queries.ApproverQueries().insertApprover(name, email) } models.queries.ApproverQueries().getAllApprovers must have size approvers.size } } - } } } diff --git a/scalajvm/test/QuoteStagingDisableSpec.scala b/scalajvm/test/QuoteStagingDisableSpec.scala new file mode 100644 index 0000000..39d4248 --- /dev/null +++ b/scalajvm/test/QuoteStagingDisableSpec.scala @@ -0,0 +1,63 @@ +import org.specs2.mutable._ + +import play.api.test._ +import play.api.test.Helpers._ +import play.api.mvc._ + +import scalikejdbc._ + +import scala.concurrent.Future + +import models.data.{StagedQuote, DisabledAction} +import models.queries.{StagedQuoteQueries, DisabledActionQueries} + +import ru.org.codingteam.loglist.dto.StagedQuoteDTO + +class QuoteStagingDisableSpec extends Specification { + "The quote stage operation" should { + "stage the quote when the operation is enabled and return StagedQuoteDTO as JSON" in { + running(FakeApplication()) { + DB localTx { implicit session => + withSQL { deleteFrom(DisabledAction) }.update().apply() + withSQL { deleteFrom(StagedQuote) }.update().apply() + } + + val request = route(FakeRequest( + Helpers.POST, + controllers.api.routes.Quotes.stageQuote().url, + FakeHeaders(), + "Hello, World" + )).get + + val dto = upickle.read[StagedQuoteDTO](contentAsString(request)) + val stagedQuote = DB localTx { implicit session => + StagedQuoteQueries().getStagedQuoteByToken(dto.token).get + } + + dto.token mustEqual stagedQuote.token + dto.content mustEqual stagedQuote.content + dto.time mustEqual stagedQuote.time.getMillis + } + } + + "return 503 code when the operation is disabled" in { + running(FakeApplication()) { + DB localTx { implicit session => + withSQL { deleteFrom(DisabledAction) }.update().apply() + withSQL { deleteFrom(StagedQuote) }.update().apply() + + DisabledActionQueries().disableAction("stage") + } + + val request = route(FakeRequest( + Helpers.POST, + controllers.api.routes.Quotes.stageQuote().url, + FakeHeaders(), + "Hello, World" + )).get + + status(request) mustEqual 503 + } + } + } +} From 98738e9138f6a55d82765f41a56a0debb60be1f5 Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 20 Apr 2015 01:58:57 +0600 Subject: [PATCH 07/14] Add spec for stage autodisable (#100) --- .travis.yml | 1 + devenv.example | 4 ++- devenv.ps1.example | 3 +- scalajvm/conf/application.conf | 5 ++- scalajvm/test/QuoteStagingDisableSpec.scala | 39 +++++++++++++++++---- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86ea53b..e91e7ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ env: APPROVAL_SMTP_HOST="smtp.loglist.net" APPROVAL_EMAIL="noreply@loglist.net" APPROVAL_EMAIL_PASSWORD="hortahell" + STAGE_AREA_COUNT_LIMIT=100 before_script: - psql -c 'create database loglist;' -U postgres - psql -c 'create extension pgcrypto;' -U postgres -d loglist diff --git a/devenv.example b/devenv.example index aa9b9c2..56e8e9c 100644 --- a/devenv.example +++ b/devenv.example @@ -9,4 +9,6 @@ export BASIC_AUTH_PASSWORD=hell export APPROVAL_SMTP_HOST="smtp.loglist.net" export APPROVAL_EMAIL="noreply@loglist.net" -export APPROVAL_EMAIL_PASSWORD="hortahell" \ No newline at end of file +export APPROVAL_EMAIL_PASSWORD="hortahell" + +export STAGE_AREA_COUNT_LIMIT=100 \ No newline at end of file diff --git a/devenv.ps1.example b/devenv.ps1.example index bca0d72..dcf714f 100644 --- a/devenv.ps1.example +++ b/devenv.ps1.example @@ -6,4 +6,5 @@ $env:BASIC_AUTH_USERNAME = 'horta' $env:BASIC_AUTH_PASSWORD = 'hell' $env:APPROVAL_SMTP_HOST = 'smtp.loglist.net' $env:APPROVAL_EMAIL = 'noreply@loglist.net' -$env:APPROVAL_EMAIL_PASSWORD = 'hortahell' \ No newline at end of file +$env:APPROVAL_EMAIL_PASSWORD = 'hortahell' +$env:STAGE_AREA_COUNT_LIMIT='100' \ No newline at end of file diff --git a/scalajvm/conf/application.conf b/scalajvm/conf/application.conf index 9677af7..e408ed4 100644 --- a/scalajvm/conf/application.conf +++ b/scalajvm/conf/application.conf @@ -77,4 +77,7 @@ basicAuth.password = ${BASIC_AUTH_PASSWORD} # Approval Notifications approval.smtpHost = ${APPROVAL_SMTP_HOST} approval.email = ${APPROVAL_EMAIL} -approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} \ No newline at end of file +approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} + +# Stage Area +stage.countLimit = ${STAGE_AREA_COUNT_LIMIT} \ No newline at end of file diff --git a/scalajvm/test/QuoteStagingDisableSpec.scala b/scalajvm/test/QuoteStagingDisableSpec.scala index 39d4248..01c40f1 100644 --- a/scalajvm/test/QuoteStagingDisableSpec.scala +++ b/scalajvm/test/QuoteStagingDisableSpec.scala @@ -2,24 +2,26 @@ import org.specs2.mutable._ import play.api.test._ import play.api.test.Helpers._ -import play.api.mvc._ import scalikejdbc._ -import scala.concurrent.Future -import models.data.{StagedQuote, DisabledAction} +import models.data.{StagedQuote, DisabledAction, Maintainer} import models.queries.{StagedQuoteQueries, DisabledActionQueries} import ru.org.codingteam.loglist.dto.StagedQuoteDTO class QuoteStagingDisableSpec extends Specification { + + def clearTable[T](table : SQLSyntaxSupport[T])(implicit db: DBSession): Unit = + withSQL { deleteFrom(table) }.update().apply() + "The quote stage operation" should { "stage the quote when the operation is enabled and return StagedQuoteDTO as JSON" in { running(FakeApplication()) { DB localTx { implicit session => - withSQL { deleteFrom(DisabledAction) }.update().apply() - withSQL { deleteFrom(StagedQuote) }.update().apply() + clearTable(DisabledAction) + clearTable(StagedQuote) } val request = route(FakeRequest( @@ -43,8 +45,8 @@ class QuoteStagingDisableSpec extends Specification { "return 503 code when the operation is disabled" in { running(FakeApplication()) { DB localTx { implicit session => - withSQL { deleteFrom(DisabledAction) }.update().apply() - withSQL { deleteFrom(StagedQuote) }.update().apply() + clearTable(DisabledAction) + clearTable(StagedQuote) DisabledActionQueries().disableAction("stage") } @@ -59,5 +61,28 @@ class QuoteStagingDisableSpec extends Specification { status(request) mustEqual 503 } } + + "disable itself when it cannot free some space in the staging area " + + "after exceeding certain limit" in { + running(FakeApplication(additionalConfiguration = Map("stage.countLimit" -> 5))) { + DB localTx { implicit session => + clearTable(DisabledAction) + clearTable(StagedQuote) + clearTable(Maintainer) + } + + val request = FakeRequest( + Helpers.POST, + controllers.api.routes.Quotes.stageQuote().url, + FakeHeaders(), + "Hello, World" + ) + + for (i <- 1 to 5) { status(route(request).get) mustEqual 200 } + status(route(request).get) mustEqual 503 + + 1 mustEqual 1 + } + } } } From 8ab9605bce7f9f35113f842c1cb90ba9aeca2973 Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 20 Apr 2015 02:04:11 +0600 Subject: [PATCH 08/14] Introduce some test helpers (#100) --- scalajvm/test/ApproverModelSpec.scala | 5 +++-- scalajvm/test/QuoteStagingDisableSpec.scala | 6 ++---- scalajvm/test/helpers/DatabaseHelpers.scala | 8 ++++++++ 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 scalajvm/test/helpers/DatabaseHelpers.scala diff --git a/scalajvm/test/ApproverModelSpec.scala b/scalajvm/test/ApproverModelSpec.scala index 0ca3f29..84ba74a 100644 --- a/scalajvm/test/ApproverModelSpec.scala +++ b/scalajvm/test/ApproverModelSpec.scala @@ -1,3 +1,4 @@ +import helpers.DatabaseHelpers import org.specs2.mutable._ import play.api.test._ @@ -7,7 +8,7 @@ import scalikejdbc._ import models.data.Approver -class ApproverModelSpec extends Specification { +class ApproverModelSpec extends Specification with DatabaseHelpers { val approvers = List( ("rexim", "rexim@loglist.net"), ("ForNeVeR", "fornever@loglist.net") @@ -17,7 +18,7 @@ class ApproverModelSpec extends Specification { "be able to add new approvers and return them back" in { running(FakeApplication()) { DB localTx { implicit session => - withSQL { deleteFrom(Approver) }.update().apply() + clearTable(Approver) for ((name, email) <- approvers) { models.queries.ApproverQueries().insertApprover(name, email) diff --git a/scalajvm/test/QuoteStagingDisableSpec.scala b/scalajvm/test/QuoteStagingDisableSpec.scala index 01c40f1..780a69a 100644 --- a/scalajvm/test/QuoteStagingDisableSpec.scala +++ b/scalajvm/test/QuoteStagingDisableSpec.scala @@ -1,3 +1,4 @@ +import helpers.DatabaseHelpers import org.specs2.mutable._ import play.api.test._ @@ -11,10 +12,7 @@ import models.queries.{StagedQuoteQueries, DisabledActionQueries} import ru.org.codingteam.loglist.dto.StagedQuoteDTO -class QuoteStagingDisableSpec extends Specification { - - def clearTable[T](table : SQLSyntaxSupport[T])(implicit db: DBSession): Unit = - withSQL { deleteFrom(table) }.update().apply() +class QuoteStagingDisableSpec extends Specification with DatabaseHelpers { "The quote stage operation" should { "stage the quote when the operation is enabled and return StagedQuoteDTO as JSON" in { diff --git a/scalajvm/test/helpers/DatabaseHelpers.scala b/scalajvm/test/helpers/DatabaseHelpers.scala new file mode 100644 index 0000000..c81748f --- /dev/null +++ b/scalajvm/test/helpers/DatabaseHelpers.scala @@ -0,0 +1,8 @@ +package helpers + +import scalikejdbc._ + +trait DatabaseHelpers { + def clearTable[T](table : SQLSyntaxSupport[T])(implicit db: DBSession): Unit = + withSQL { deleteFrom(table) }.update().apply() +} From abe6dfee705128a0dc4a4198cc70b2ecfbb5639d Mon Sep 17 00:00:00 2001 From: rexim Date: Sun, 3 May 2015 23:32:32 +0600 Subject: [PATCH 09/14] Revert disableable action feature (#100) This shit is overcomplicated. --- .travis.yml | 1 - devenv.example | 4 +- devenv.ps1.example | 3 +- scalajvm/app/controllers/api/Quotes.scala | 4 +- .../app/helpers/DisableableActionWithTx.scala | 17 ---- scalajvm/app/models/data/DisabledAction.scala | 10 --- .../queries/DisabledActionQueries.scala | 28 ------ scalajvm/conf/application.conf | 5 +- scalajvm/conf/evolutions/default/8.sql | 5 -- scalajvm/test/QuoteStagingDisableSpec.scala | 86 ------------------- 10 files changed, 5 insertions(+), 158 deletions(-) delete mode 100644 scalajvm/app/helpers/DisableableActionWithTx.scala delete mode 100644 scalajvm/app/models/data/DisabledAction.scala delete mode 100644 scalajvm/app/models/queries/DisabledActionQueries.scala delete mode 100644 scalajvm/test/QuoteStagingDisableSpec.scala diff --git a/.travis.yml b/.travis.yml index e91e7ef..86ea53b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,6 @@ env: APPROVAL_SMTP_HOST="smtp.loglist.net" APPROVAL_EMAIL="noreply@loglist.net" APPROVAL_EMAIL_PASSWORD="hortahell" - STAGE_AREA_COUNT_LIMIT=100 before_script: - psql -c 'create database loglist;' -U postgres - psql -c 'create extension pgcrypto;' -U postgres -d loglist diff --git a/devenv.example b/devenv.example index 56e8e9c..aa9b9c2 100644 --- a/devenv.example +++ b/devenv.example @@ -9,6 +9,4 @@ export BASIC_AUTH_PASSWORD=hell export APPROVAL_SMTP_HOST="smtp.loglist.net" export APPROVAL_EMAIL="noreply@loglist.net" -export APPROVAL_EMAIL_PASSWORD="hortahell" - -export STAGE_AREA_COUNT_LIMIT=100 \ No newline at end of file +export APPROVAL_EMAIL_PASSWORD="hortahell" \ No newline at end of file diff --git a/devenv.ps1.example b/devenv.ps1.example index dcf714f..bca0d72 100644 --- a/devenv.ps1.example +++ b/devenv.ps1.example @@ -6,5 +6,4 @@ $env:BASIC_AUTH_USERNAME = 'horta' $env:BASIC_AUTH_PASSWORD = 'hell' $env:APPROVAL_SMTP_HOST = 'smtp.loglist.net' $env:APPROVAL_EMAIL = 'noreply@loglist.net' -$env:APPROVAL_EMAIL_PASSWORD = 'hortahell' -$env:STAGE_AREA_COUNT_LIMIT='100' \ No newline at end of file +$env:APPROVAL_EMAIL_PASSWORD = 'hortahell' \ No newline at end of file diff --git a/scalajvm/app/controllers/api/Quotes.scala b/scalajvm/app/controllers/api/Quotes.scala index c9ad87a..c55f828 100644 --- a/scalajvm/app/controllers/api/Quotes.scala +++ b/scalajvm/app/controllers/api/Quotes.scala @@ -1,6 +1,6 @@ package controllers.api -import helpers._ +import helpers.ActionWithTx import models.queries._ import models.data._ import play.api.mvc._ @@ -18,7 +18,7 @@ object Quotes extends Controller { prepareResponse(QuoteQueries().getRandomQuote) } - def stageQuote = DisableableActionWithTx("stage", "Staging is disabled") { request => + def stageQuote = ActionWithTx { request => import request.dbSession request.body.asText match { case Some(content) => { diff --git a/scalajvm/app/helpers/DisableableActionWithTx.scala b/scalajvm/app/helpers/DisableableActionWithTx.scala deleted file mode 100644 index 24166b9..0000000 --- a/scalajvm/app/helpers/DisableableActionWithTx.scala +++ /dev/null @@ -1,17 +0,0 @@ -package helpers - -import models.queries.DisabledActionQueries -import play.api.mvc._ -import scalikejdbc._ - -import scala.concurrent._ -import ExecutionContext.Implicits.global - -case class DisableableActionWithTx(name: String, message: String) extends ActionBuilder[RequestWithSession] with Results { - override def invokeBlock[A](request: Request[A], block: RequestWithSession[A] => Future[Result]): Future[Result] = { - DB localTx { implicit session => - if (DisabledActionQueries().isActionDisabled(name)) Future(ServiceUnavailable(message).as("text/plain")) - else block(new RequestWithSession[A](request)) - } - } -} diff --git a/scalajvm/app/models/data/DisabledAction.scala b/scalajvm/app/models/data/DisabledAction.scala deleted file mode 100644 index 9243534..0000000 --- a/scalajvm/app/models/data/DisabledAction.scala +++ /dev/null @@ -1,10 +0,0 @@ -package models.data - -import scalikejdbc._ - -case class DisabledAction(name: String) -object DisabledAction extends SQLSyntaxSupport[DisabledAction] { - override val tableName = "disabled_action" - def apply(rs: WrappedResultSet): DisabledAction = - new DisabledAction(rs.string("name")) -} diff --git a/scalajvm/app/models/queries/DisabledActionQueries.scala b/scalajvm/app/models/queries/DisabledActionQueries.scala deleted file mode 100644 index 2e4272e..0000000 --- a/scalajvm/app/models/queries/DisabledActionQueries.scala +++ /dev/null @@ -1,28 +0,0 @@ -package models.queries - -import models.data.DisabledAction - -import scalikejdbc._ - -case class DisabledActionQueries(implicit session: DBSession) { - def isActionDisabled(name: String): Boolean = { - val da = DisabledAction.syntax("da") - withSQL { - select(da.*).from(DisabledAction as da).where.eq(da.name, name) - }.map(rs => DisabledAction(rs)).first().apply().isDefined - } - - def disableAction(name: String): Unit = { - val da = DisabledAction.column - if (!isActionDisabled(name)) withSQL { - insert.into(DisabledAction).columns(da.name).values(name) - }.update().apply() - } - - def enableAction(name: String): Unit = { - val da = DisabledAction.column - if (isActionDisabled(name)) withSQL { - delete.from(DisabledAction).where.eq(da.name, name) - }.update().apply() - } -} diff --git a/scalajvm/conf/application.conf b/scalajvm/conf/application.conf index e408ed4..9677af7 100644 --- a/scalajvm/conf/application.conf +++ b/scalajvm/conf/application.conf @@ -77,7 +77,4 @@ basicAuth.password = ${BASIC_AUTH_PASSWORD} # Approval Notifications approval.smtpHost = ${APPROVAL_SMTP_HOST} approval.email = ${APPROVAL_EMAIL} -approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} - -# Stage Area -stage.countLimit = ${STAGE_AREA_COUNT_LIMIT} \ No newline at end of file +approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} \ No newline at end of file diff --git a/scalajvm/conf/evolutions/default/8.sql b/scalajvm/conf/evolutions/default/8.sql index 09470a6..13d06f8 100644 --- a/scalajvm/conf/evolutions/default/8.sql +++ b/scalajvm/conf/evolutions/default/8.sql @@ -13,11 +13,6 @@ create table if not exists maintainer ( email varchar not null ); -create table if not exists disabled_action ( - name varchar not null unique -); - # --- !Downs -drop table if exists disabled_action; drop table if exists maintainer; drop table if exists staged_quote; diff --git a/scalajvm/test/QuoteStagingDisableSpec.scala b/scalajvm/test/QuoteStagingDisableSpec.scala deleted file mode 100644 index 780a69a..0000000 --- a/scalajvm/test/QuoteStagingDisableSpec.scala +++ /dev/null @@ -1,86 +0,0 @@ -import helpers.DatabaseHelpers -import org.specs2.mutable._ - -import play.api.test._ -import play.api.test.Helpers._ - -import scalikejdbc._ - - -import models.data.{StagedQuote, DisabledAction, Maintainer} -import models.queries.{StagedQuoteQueries, DisabledActionQueries} - -import ru.org.codingteam.loglist.dto.StagedQuoteDTO - -class QuoteStagingDisableSpec extends Specification with DatabaseHelpers { - - "The quote stage operation" should { - "stage the quote when the operation is enabled and return StagedQuoteDTO as JSON" in { - running(FakeApplication()) { - DB localTx { implicit session => - clearTable(DisabledAction) - clearTable(StagedQuote) - } - - val request = route(FakeRequest( - Helpers.POST, - controllers.api.routes.Quotes.stageQuote().url, - FakeHeaders(), - "Hello, World" - )).get - - val dto = upickle.read[StagedQuoteDTO](contentAsString(request)) - val stagedQuote = DB localTx { implicit session => - StagedQuoteQueries().getStagedQuoteByToken(dto.token).get - } - - dto.token mustEqual stagedQuote.token - dto.content mustEqual stagedQuote.content - dto.time mustEqual stagedQuote.time.getMillis - } - } - - "return 503 code when the operation is disabled" in { - running(FakeApplication()) { - DB localTx { implicit session => - clearTable(DisabledAction) - clearTable(StagedQuote) - - DisabledActionQueries().disableAction("stage") - } - - val request = route(FakeRequest( - Helpers.POST, - controllers.api.routes.Quotes.stageQuote().url, - FakeHeaders(), - "Hello, World" - )).get - - status(request) mustEqual 503 - } - } - - "disable itself when it cannot free some space in the staging area " + - "after exceeding certain limit" in { - running(FakeApplication(additionalConfiguration = Map("stage.countLimit" -> 5))) { - DB localTx { implicit session => - clearTable(DisabledAction) - clearTable(StagedQuote) - clearTable(Maintainer) - } - - val request = FakeRequest( - Helpers.POST, - controllers.api.routes.Quotes.stageQuote().url, - FakeHeaders(), - "Hello, World" - ) - - for (i <- 1 to 5) { status(route(request).get) mustEqual 200 } - status(route(request).get) mustEqual 503 - - 1 mustEqual 1 - } - } - } -} From 800c89a8f58c1310cc8b7b071e3ddebeb429de61 Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 4 May 2015 00:12:02 +0600 Subject: [PATCH 10/14] Add staging specification (#100) --- scalajvm/test/StagingSpec.scala | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scalajvm/test/StagingSpec.scala diff --git a/scalajvm/test/StagingSpec.scala b/scalajvm/test/StagingSpec.scala new file mode 100644 index 0000000..8f52b10 --- /dev/null +++ b/scalajvm/test/StagingSpec.scala @@ -0,0 +1,39 @@ +import helpers.DatabaseHelpers +import org.specs2.mutable._ + +import play.api.test._ +import play.api.test.Helpers._ + +import scalikejdbc._ + +import models.data.StagedQuote +import models.queries.StagedQuoteQueries +import ru.org.codingteam.loglist.dto.StagedQuoteDTO + +class StagingSpec extends Specification with DatabaseHelpers { + "The quote stage operation" should { + "stage the quote when the operation and return StagedQuoteDTO as JSON" in { + running(FakeApplication()) { + DB localTx { implicit session => + withSQL { deleteFrom(StagedQuote) }.update().apply() + } + + val request = route(FakeRequest( + Helpers.POST, + controllers.api.routes.Quotes.stageQuote().url, + FakeHeaders(), + "Hello, World" + )).get + + val dto = upickle.read[StagedQuoteDTO](contentAsString(request)) + val stagedQuote = DB localTx { implicit session => + StagedQuoteQueries().getStagedQuoteByToken(dto.token).get + } + + dto.token mustEqual stagedQuote.token + dto.content mustEqual stagedQuote.content + dto.time mustEqual stagedQuote.time.getMillis + } + } + } +} From 32b5a60cb169afaedc651f2fff2e5b1fd6df0dbe Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 4 May 2015 00:13:00 +0600 Subject: [PATCH 11/14] Fix a message in staging specs (#100) --- scalajvm/test/StagingSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scalajvm/test/StagingSpec.scala b/scalajvm/test/StagingSpec.scala index 8f52b10..9366f63 100644 --- a/scalajvm/test/StagingSpec.scala +++ b/scalajvm/test/StagingSpec.scala @@ -12,7 +12,7 @@ import ru.org.codingteam.loglist.dto.StagedQuoteDTO class StagingSpec extends Specification with DatabaseHelpers { "The quote stage operation" should { - "stage the quote when the operation and return StagedQuoteDTO as JSON" in { + "stage the quote and return StagedQuoteDTO as JSON" in { running(FakeApplication()) { DB localTx { implicit session => withSQL { deleteFrom(StagedQuote) }.update().apply() From 6af379efd769ea2b6e2cb8d1cff148cc569bbb60 Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 4 May 2015 01:09:50 +0600 Subject: [PATCH 12/14] Specs for autofilling submit form (#100) --- scalajvm/test/StagingSpec.scala | 36 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/scalajvm/test/StagingSpec.scala b/scalajvm/test/StagingSpec.scala index 9366f63..68464c5 100644 --- a/scalajvm/test/StagingSpec.scala +++ b/scalajvm/test/StagingSpec.scala @@ -11,19 +11,22 @@ import models.queries.StagedQuoteQueries import ru.org.codingteam.loglist.dto.StagedQuoteDTO class StagingSpec extends Specification with DatabaseHelpers { + + def stageRequestForText(text: String) = FakeRequest( + Helpers.POST, + controllers.api.routes.Quotes.stageQuote().url, + FakeHeaders(), + text + ) + "The quote stage operation" should { "stage the quote and return StagedQuoteDTO as JSON" in { running(FakeApplication()) { DB localTx { implicit session => - withSQL { deleteFrom(StagedQuote) }.update().apply() + clearTable(StagedQuote) } - val request = route(FakeRequest( - Helpers.POST, - controllers.api.routes.Quotes.stageQuote().url, - FakeHeaders(), - "Hello, World" - )).get + val request = route(stageRequestForText("Hello, World")).get val dto = upickle.read[StagedQuoteDTO](contentAsString(request)) val stagedQuote = DB localTx { implicit session => @@ -36,4 +39,23 @@ class StagingSpec extends Specification with DatabaseHelpers { } } } + + "The submit form page" should { + "contain the staged quote if you provide appropriate stagedQuoteToken" in { + running(FakeApplication()) { + DB localTx { implicit session => + clearTable(StagedQuote) + } + + val expectedText = "Foo, Bar" + + val stageResponse = route(stageRequestForText(expectedText)).get + val dto = upickle.read[StagedQuoteDTO](contentAsString(stageResponse)) + + val submitFormResponse = controllers.SuggestedQuotes.newQuote(dto.token)(FakeRequest()) + + contentAsString(submitFormResponse) must contain(expectedText) + } + } + } } From 9d088f2d43237ae9aebd2464c5998c98aae030bb Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 4 May 2015 03:06:32 +0600 Subject: [PATCH 13/14] Implement exceeding count limit (#100) --- scalajvm/app/controllers/api/Quotes.scala | 20 +++++++++++++++---- .../models/queries/StagedQuoteQueries.scala | 18 ++++++++++++++++- scalajvm/conf/application.conf | 6 +++++- scalajvm/test/StagingSpec.scala | 14 +++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/scalajvm/app/controllers/api/Quotes.scala b/scalajvm/app/controllers/api/Quotes.scala index c55f828..6a9664a 100644 --- a/scalajvm/app/controllers/api/Quotes.scala +++ b/scalajvm/app/controllers/api/Quotes.scala @@ -4,10 +4,16 @@ import helpers.ActionWithTx import models.queries._ import models.data._ import play.api.mvc._ +import play.api.Play.current import ru.org.codingteam.loglist.dto.{StagedQuoteDTO, QuoteDTO} object Quotes extends Controller { + private val stagingMaxCount = + current.configuration.getInt("staging.maxCount").get + private val stagingLifeTimePeriod = + current.configuration.getInt("staging.lifeTimePeriod").get + def getQuote(id: Long) = ActionWithTx { request => import request.dbSession prepareResponse(QuoteQueries().getQuoteById(id)) @@ -22,10 +28,16 @@ object Quotes extends Controller { import request.dbSession request.body.asText match { case Some(content) => { - val id = StagedQuoteQueries().insertStagedQuote(content, Some(request.remoteAddress)) - StagedQuoteQueries().getStagedQuoteById(id) match { - case Some(stagedQuote) => Ok(upickle.write(buildStagedQuoteDto(stagedQuote))).as("application/json; charset=utf-8") - case None => InternalServerError("The quote was not staged properly").as("text/plain") + StagedQuoteQueries().deleteOldStagedQuotes(stagingLifeTimePeriod) + + if (StagedQuoteQueries().countStagedQuotes() < stagingMaxCount) { + val id = StagedQuoteQueries().insertStagedQuote(content, Some(request.remoteAddress)) + StagedQuoteQueries().getStagedQuoteById(id) match { + case Some(stagedQuote) => Ok(upickle.write(buildStagedQuoteDto(stagedQuote))).as("application/json; charset=utf-8") + case None => InternalServerError("The quote was not staged properly").as("text/plain") + } + } else { + InsufficientStorage("Staged quotes count limit exceeded. Please try again later.").as("text/plain") } } case None => BadRequest("The request body was not found!").as("text/plain") diff --git a/scalajvm/app/models/queries/StagedQuoteQueries.scala b/scalajvm/app/models/queries/StagedQuoteQueries.scala index 2bc063e..aba352f 100644 --- a/scalajvm/app/models/queries/StagedQuoteQueries.scala +++ b/scalajvm/app/models/queries/StagedQuoteQueries.scala @@ -22,7 +22,23 @@ case class StagedQuoteQueries(implicit session: DBSession) { def insertStagedQuote(content: String, stagerIp: Option[String]) = { val sq = StagedQuote.column withSQL { - insert.into(StagedQuote).columns(sq.time, sq.content, sq.stagerIp).values(DateTime.now(), content, stagerIp) + insert.into(StagedQuote) + .columns(sq.time, sq.content, sq.stagerIp) + .values(DateTime.now(), content, stagerIp) }.updateAndReturnGeneratedKey().apply() } + + def countStagedQuotes() = { + val sq = StagedQuote.syntax("sq") + withSQL { + select(sqls.count).from(StagedQuote as sq) + }.map(rs => rs.int(1)).first().apply().getOrElse(0) + } + + def deleteOldStagedQuotes(interval: Int): Boolean = { + val sq = StagedQuote.column + withSQL { + delete.from(StagedQuote).where.lt(sq.time, DateTime.now().minusMinutes(interval)) + }.update().apply() != 0 + } } diff --git a/scalajvm/conf/application.conf b/scalajvm/conf/application.conf index 9677af7..0379e9a 100644 --- a/scalajvm/conf/application.conf +++ b/scalajvm/conf/application.conf @@ -77,4 +77,8 @@ basicAuth.password = ${BASIC_AUTH_PASSWORD} # Approval Notifications approval.smtpHost = ${APPROVAL_SMTP_HOST} approval.email = ${APPROVAL_EMAIL} -approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} \ No newline at end of file +approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} + +# Staging +staging.maxCount = ${STAGING_MAX_COUNT} +staging.lifeTimePeriod = ${STAGING_LIFE_TIME_PERIOD} \ No newline at end of file diff --git a/scalajvm/test/StagingSpec.scala b/scalajvm/test/StagingSpec.scala index 68464c5..7da8471 100644 --- a/scalajvm/test/StagingSpec.scala +++ b/scalajvm/test/StagingSpec.scala @@ -3,6 +3,7 @@ import org.specs2.mutable._ import play.api.test._ import play.api.test.Helpers._ +import play.api.Play.current import scalikejdbc._ @@ -38,6 +39,19 @@ class StagingSpec extends Specification with DatabaseHelpers { dto.time mustEqual stagedQuote.time.getMillis } } + + "should return 507 in case count limit is exceeded" in { + running(FakeApplication()) { + DB localTx { implicit session => + clearTable(StagedQuote) + } + + val stagingText = "Hello, World" + + for (i <- 1 to 5) { status(route(stageRequestForText(stagingText)).get) mustEqual 200 } + status(route(stageRequestForText(stagingText)).get) mustEqual 507 + } + } } "The submit form page" should { From b37c56e4c8dfccb5b8664e836b7292f88d71ecf9 Mon Sep 17 00:00:00 2001 From: rexim Date: Mon, 4 May 2015 03:15:04 +0600 Subject: [PATCH 14/14] Remove Maintainer entity (#100) --- scalajvm/app/models/data/Maintainer.scala | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 scalajvm/app/models/data/Maintainer.scala diff --git a/scalajvm/app/models/data/Maintainer.scala b/scalajvm/app/models/data/Maintainer.scala deleted file mode 100644 index a69da78..0000000 --- a/scalajvm/app/models/data/Maintainer.scala +++ /dev/null @@ -1,10 +0,0 @@ -package models.data - -import scalikejdbc._ - -case class Maintainer(id: Long, name: String, email: String) -object Maintainer extends SQLSyntaxSupport[Maintainer] { - override val tableName = "maintainer" - def apply(rs: WrappedResultSet): Maintainer = - new Maintainer(rs.long("id"), rs.string("name"), rs.string("email")) -}