app/controllers/banner/BannerDesignsController.scala (201 lines of code) (raw):
package controllers.banner
import com.gu.googleauth.AuthAction
import models.DynamoErrors.{DynamoDuplicateNameError, DynamoError, DynamoNoLockError}
import models.{BannerDesign, BannerTest, Channel}
import play.api.libs.circe.Circe
import play.api.mvc.{AbstractController, ActionBuilder, AnyContent, ControllerComponents, Result}
import services.{DynamoArchivedBannerDesigns, DynamoBannerDesigns, DynamoChannelTests}
import services.S3Client.S3ObjectSettings
import utils.Circe.noNulls
import zio.{IO, ZEnv, ZIO}
import com.typesafe.scalalogging.LazyLogging
import io.circe.syntax.EncoderOps
import io.circe.generic.auto._
import models.BannerUI
import scala.concurrent.{ExecutionContext, Future}
class BannerDesignsController(
authAction: ActionBuilder[AuthAction.UserIdentityRequest, AnyContent],
components: ControllerComponents,
stage: String,
runtime: zio.Runtime[ZEnv],
dynamoDesigns: DynamoBannerDesigns,
dynamoTests: DynamoChannelTests,
dynamoArchivedDesigns: DynamoArchivedBannerDesigns
)(implicit ec: ExecutionContext)
extends AbstractController(components)
with Circe
with LazyLogging {
case class BannerDesignsResponse(
bannerDesigns: List[BannerDesign],
userEmail: String,
)
val lockFileName = "banner-designs"
private val lockObjectSettings = S3ObjectSettings(
bucket = "support-admin-console",
key = s"$stage/locks/$lockFileName.lock",
publicRead = false,
cacheControl = None
)
val s3Client = services.S3
private def run(f: => ZIO[ZEnv, Throwable, Result]): Future[Result] =
runtime.unsafeRunToFuture {
f.catchAll(error => {
logger.error(
s"Returning InternalServerError to client: ${error.getMessage}",
error)
IO.succeed(InternalServerError(error.getMessage))
})
}
def getAll = authAction.async { request =>
run {
dynamoDesigns
.getAllBannerDesigns()
.map { bannerDesigns =>
val response = BannerDesignsResponse(
bannerDesigns,
request.user.email
)
Ok(noNulls(response.asJson))
}
}
}
/**
* Handlers for design editing
*/
def get(designName: String) = authAction.async { request =>
run {
dynamoDesigns
.getBannerDesign(designName)
.map(bannerDesign => Ok(noNulls(bannerDesign.asJson)))
}
}
def update = authAction.async(circe.json[BannerDesign]) { request =>
run {
val design = request.body
logger.info(s"${request.user.email} is updating '${design.name}'")
dynamoDesigns
.updateBannerDesign(design, request.user.email)
.map(_ => Ok("updated"))
.catchSome {
case DynamoNoLockError(error) =>
logger.warn(
s"Failed to save '${design.name}' because user ${request.user.email} does not have it locked: ${error.getMessage}")
IO.succeed(Conflict(
s"You do not currently have design '${design.name}' open for edit"))
}
}
}
def create = authAction.async(circe.json[BannerDesign]) { request =>
run {
val design = request.body
logger.info(s"${request.user.email} is creating '${design.name}'")
dynamoDesigns
.createBannerDesign(design)
.map(_ => Ok("created"))
.catchSome {
case DynamoDuplicateNameError(error) =>
logger.warn(
s"Failed to create '${design.name}' because name already exists: ${error.getMessage}")
IO.succeed(BadRequest(
s"Cannot create design '${design.name}' because it already exists. Please use a different name"))
}
}
}
def lock(designName: String) = authAction.async { request =>
run {
logger.info(s"${request.user.email} is locking '$designName'")
dynamoDesigns
.lockBannerDesign(designName, request.user.email, force = false)
.map(_ => Ok("locked"))
.catchSome {
case DynamoNoLockError(error) =>
logger.warn(
s"Failed to lock '$designName' because it is already locked: ${error.getMessage}")
IO.succeed(Conflict(
s"Design '$designName' is already locked for edit by another user"))
}
}
}
def unlock(designName: String) = authAction.async { request =>
run {
logger.info(s"${request.user.email} is unlocking '$designName'")
dynamoDesigns
.unlockBannerDesign(designName, request.user.email)
.map(_ => Ok("unlocked"))
.catchSome {
case DynamoNoLockError(error) =>
logger.warn(
s"Failed to unlock '$designName' because user ${request.user.email} does not have it locked: ${error.getMessage}")
IO.succeed(Conflict(
s"You do not currently have design '$designName' open for edit"))
}
}
}
def forceLock(designName: String) = authAction.async { request =>
run {
logger.info(s"${request.user.email} is force locking '$designName'")
dynamoDesigns
.lockBannerDesign(designName, request.user.email, force = true)
.map(_ => Ok("locked"))
}
}
private def parseStatus(
rawStatus: String): Option[models.BannerDesignStatus] =
rawStatus.toLowerCase match {
case "live" => Some(models.BannerDesignStatus.Live)
case "draft" => Some(models.BannerDesignStatus.Draft)
case _ => None
}
private def getAllBannerTests(): ZIO[ZEnv, DynamoError, List[BannerTest]] = {
import models.BannerTest._
ZIO.collectAllPar(List(
dynamoTests.getAllTests(Channel.Banner1),
dynamoTests.getAllTests(Channel.Banner2)
)).map(_.flatten)
}
// Returns any tests currently using the design
private def getTestsUsingDesign(designName: String): ZIO[ZEnv, DynamoError, List[BannerTest]] =
getAllBannerTests().map { bannerTests =>
bannerTests
.filter(banner => banner.variants.exists(variant => variant.template match {
case BannerUI(name) if designName == name => true
case _ => false
}))
}
def setStatus(designName: String, rawStatus: String) =
authAction.async { request =>
run {
logger.info(
s"${request.user.email} is changing status to $rawStatus on: $designName")
parseStatus(rawStatus) match {
case Some(status) =>
// First make sure no test variants are currently using this design
getTestsUsingDesign(designName)
.flatMap {
case Nil =>
dynamoDesigns
.updateStatus(designName, status)
.map(_ => Ok(status.toString))
case tests =>
val testNames = tests.map(banner => banner.name)
ZIO.succeed(BadRequest(s"Cannot change status of design $designName because it's still in use by the following test(s): ${testNames.mkString(", ")}"))
}
case None =>
ZIO.succeed(BadRequest(s"Invalid status for design: $rawStatus"))
}
}
}
def archive(designName: String) =
authAction.async { request =>
run {
logger.info(s"${request.user.email} is archiving banner design: $designName")
dynamoDesigns
.getRawDesign(designName)
// write to the archive table
.flatMap(dynamoArchivedDesigns.putRaw)
// now delete from the main table
.flatMap(_ => dynamoDesigns.deleteBannerDesign(designName))
.map(_ => Ok("archived"))
}
}
def usage(designName: String) =
authAction.async { request =>
run {
getTestsUsingDesign(designName)
.map(testNames => Ok(testNames.asJson))
}
}
}