app/controllers/PlutoCommissionController.scala (248 lines of code) (raw):
package controllers
import akka.actor.ActorRef
import auth.BearerTokenAuth
import exceptions.RecordNotFoundException
import helpers.AllowCORSFunctions
import javax.inject._
import models._
import play.api.Configuration
import play.api.cache.SyncCacheApi
import play.api.db.slick.DatabaseConfigProvider
import play.api.http.HttpEntity
import play.api.libs.json.{JsError, JsResult, JsValue, Json}
import play.api.mvc.{ControllerComponents, Request, ResponseHeader, Result}
import services.{CommissionStatusPropagator, CreateOperation, UpdateOperation}
import slick.jdbc.PostgresProfile
import slick.jdbc.PostgresProfile.api._
import slick.lifted.TableQuery
import scala.collection.immutable.ListMap
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
import scala.concurrent.ExecutionContext.Implicits.global
@Singleton
class PlutoCommissionController @Inject()(override val controllerComponents:ControllerComponents,
override val bearerTokenAuth:BearerTokenAuth,
dbConfigProvider:DatabaseConfigProvider,
cacheImpl:SyncCacheApi,
override implicit val config:Configuration,
@Named("commission-status-propagator") commissionStatusPropagator:ActorRef,
@Named("rabbitmq-propagator") rabbitMqPropagator:ActorRef)
extends GenericDatabaseObjectControllerWithFilter[PlutoCommission,PlutoCommissionFilterTerms]
with PlutoCommissionSerializer with PlutoCommissionFilterTermsSerializer {
implicit val db = dbConfigProvider.get[PostgresProfile].db
implicit val cache:SyncCacheApi = cacheImpl
object SortDirection extends Enumeration {
val desc, asc = Value
}
def withRequiredSort(query: =>Query[PlutoCommissionRow, PlutoCommission, Seq], sort:String, sortDirection:SortDirection.Value):Query[PlutoCommissionRow, PlutoCommission, Seq] = {
import EntryStatusMapper._
(sort, sortDirection) match {
case ("created", SortDirection.desc) => query.sortBy(_.created.desc)
case ("created", SortDirection.asc) => query.sortBy(_.created.asc)
case ("title", SortDirection.desc) => query.sortBy(_.title.desc)
case ("title", SortDirection.asc) => query.sortBy(_.title.asc)
case ("workingGroupId", SortDirection.desc) => query.sortBy(_.workingGroup.desc)
case ("workingGroupId", SortDirection.asc) => query.sortBy(_.workingGroup.asc)
case ("status", SortDirection.desc) => query.sortBy(_.status.desc)
case ("status", SortDirection.asc) => query.sortBy(_.status.asc)
case ("owner", SortDirection.desc) => query.sortBy(_.owner.desc)
case ("owner", SortDirection.asc) => query.sortBy(_.owner.asc)
case ("projectCount", SortDirection.desc) => query.sortBy(_.created.desc)
case ("projectCount", SortDirection.asc) => query.sortBy(_.created.asc)
case _ =>
logger.warn(s"Sort field $sort was not recognised, ignoring")
query
}
}
private def getSortDirection(directionString:String):Option[SortDirection.Value] = Try { SortDirection.withName(directionString) }.toOption
def listFilteredAndSorted(startAt:Int, limit:Int, sort: String, sortDirection: String) = IsAuthenticatedAsync(parse.json) {uid=>{request=>
this.validateFilterParams(request).fold(
errors => {
logger.error(s"errors parsing content: $errors")
Future(BadRequest(Json.obj("status"->"error","detail"->JsError.toJson(errors))))
},
filterTerms => {
this.selectFilteredAndSorted(startAt, limit, filterTerms, sort, getSortDirection(sortDirection).getOrElse(SortDirection.desc)).map({
case Success((count,result))=>Ok(Json.obj("status" -> "ok","count"->count,"result"->this.jstranslate(result)))
case Failure(error)=>
logger.error(error.toString)
InternalServerError(Json.obj("status"->"error", "detail"->error.toString))
}
)
}
)
}}
def selectall(startAt: Int, limit: Int): Future[Try[(Int, Seq[PlutoCommission])]] = {
val results: Future[(Int, Seq[PlutoCommission])] = db.run(
TableQuery[PlutoCommissionRow].length.result.zip(
TableQuery[PlutoCommissionRow].sortBy(_.created.desc).drop(startAt).take(limit).result
)
)
results.flatMap { result => {
val count=result._1
val commissions=result._2 //wouldn't implicitly unpack for some reason!
calculateProjectCount(commissions).map(counts => {
commissions.foreach(commission => commission.projectCount = commission.id.flatMap(counts.get).orElse(Some(0)))
Success((count,commissions))
})
}}.recover(Failure(_))
}
override def selectid(requestedId: Int): Future[Try[Seq[PlutoCommission]]] = db.run(
TableQuery[PlutoCommissionRow].filter(_.id===requestedId).result.asTry
)
override def selectFiltered(startAt: Int, limit: Int, terms: PlutoCommissionFilterTerms): Future[Try[(Int, Seq[PlutoCommission])]] = {
val basequery = terms.addFilterTerms {
TableQuery[PlutoCommissionRow]
}
val results: Future[(Int, Seq[PlutoCommission])] = db.run(
basequery.length.result.zip(
basequery.sortBy(_.created.desc).drop(startAt).take(limit).result
)
)
results.flatMap { result => {
val count=result._1
val commissions=result._2
calculateProjectCount(commissions).map(counts => {
commissions.foreach(commission => commission.projectCount = commission.id.flatMap(counts.get).orElse(Some(0)))
Success((count,commissions))
})
}}.recover(Failure(_))
}
def selectFilteredAndSorted(startAt: Int, limit: Int, terms: PlutoCommissionFilterTerms, sort: String, sortDirection: SortDirection.Value): Future[Try[(Int, Seq[PlutoCommission])]] = {
val basequery = terms.addFilterTerms {
TableQuery[PlutoCommissionRow]
}
val results: Future[(Int, Seq[PlutoCommission])] = db.run(
basequery.length.result.zip(
withRequiredSort(basequery, sort, sortDirection).drop(startAt).take(limit).result
)
)
results.flatMap { result => {
val count=result._1
val commissions=result._2
calculateProjectCount(commissions).map(counts => {
commissions.foreach(commission => commission.projectCount = commission.id.flatMap(counts.get).orElse(Some(0)))
Success((count,commissions))
})
}}.recover(Failure(_))
}
def calculateProjectCount(entries: Seq[PlutoCommission]): Future[ListMap[Int, Int]] = {
val commissionIds = entries.flatMap(entry => entry.id)
db.run (
TableQuery[ProjectEntryRow].filter(_.commission inSet commissionIds)
.groupBy(_.commission)
.map({ case (commissionId, group) => (commissionId, group.length) })
.result
.map(rows => rows.collect { case (Some(commissionId), count) => (commissionId, count) } )
.map(ListMap.from)
)
}
override def insert(entry: PlutoCommission, uid: String): Future[Try[Int]] = {
val correctedEntry = entry.copy(owner = entry.owner.toLowerCase)
db.run(
(TableQuery[PlutoCommissionRow] returning TableQuery[PlutoCommissionRow].map(_.id) += correctedEntry).asTry)
.map(id => {
sendToRabbitMq(CreateOperation(), id, rabbitMqPropagator)
id
})
}
override def deleteid(requestedId: Int):Future[Try[Int]] = throw new RuntimeException("This is not supported")
override def dbupdate(itemId: Int, entry:PlutoCommission):Future[Try[Int]] = {
val newRecord = entry.id match {
case Some(_)=>entry
case None=>entry.copy(id=Some(itemId))
}
db.run(TableQuery[PlutoCommissionRow].filter(_.id===itemId).update(newRecord).asTry)
.map(maybeRows=>{
commissionStatusPropagator ! CommissionStatusPropagator.CommissionStatusUpdate(itemId, newRecord.status)
sendToRabbitMq(UpdateOperation(),newRecord,rabbitMqPropagator)
maybeRows
})
}
/*these are handled through implict translation*/
override def jstranslate(result:Seq[PlutoCommission]):Json.JsValueWrapper = result
override def jstranslate(result:PlutoCommission):Json.JsValueWrapper = result
override def validate(request: Request[JsValue]): JsResult[PlutoCommission] = request.body.validate[PlutoCommission]
override def validateFilterParams(request: Request[JsValue]): JsResult[PlutoCommissionFilterTerms] = request.body.validate[PlutoCommissionFilterTerms]
/**
* respond to CORS options requests for login from vaultdoor
* see https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
* @return
*/
def listOptions = Action { request=>
AllowCORSFunctions.checkCorsOrigins(config, request) match {
case Right(allowedOrigin) =>
val returnHeaders = Map(
"Access-Control-Allow-Methods" -> "PUT, OPTIONS",
"Access-Control-Allow-Origin" -> allowedOrigin,
"Access-Control-Allow-Headers" -> "content-type",
)
Result(
ResponseHeader(204, returnHeaders),
HttpEntity.NoEntity
)
case Left(other) =>
logger.warn(s"Invalid CORS preflight request for authentication: $other")
Forbidden("")
}
}
private def updateStatusColumn(commissionId:Int, newValue:EntryStatus.Value) = {
import EntryStatusMapper._
db.run {
val q = for {c <- TableQuery[PlutoCommissionRow] if c.id === commissionId} yield c.status
q.update(newValue)
}
}
def updateStatus(commissionId: Int) = IsAuthenticatedAsync(parse.json) {uid=> request=>
import PlutoCommissionStatusUpdateRequestSerializer._
request.body.validate[PlutoCommissionStatusUpdateRequest].fold(
invalidErrs=>
Future(BadRequest(Json.obj("status"->"bad_request","detail"->JsError.toJson(invalidErrs)))),
requiredUpdate=>
updateStatusColumn(commissionId, requiredUpdate.status).map(rowsUpdated=>{
if(rowsUpdated==0){
NotFound(Json.obj("status"->"not_found","detail"->s"No commission with id $commissionId"))
} else {
if(rowsUpdated>1) logger.error(s"Status update request for commission $commissionId returned $rowsUpdated rows updated, expected 1! This indicates a database problem")
commissionStatusPropagator ! CommissionStatusPropagator.CommissionStatusUpdate(commissionId, requiredUpdate.status)
sendToRabbitMq(UpdateOperation(), commissionId, rabbitMqPropagator).foreach(_ => ())
Ok(Json.obj("status"->"ok","detail"->"commission status updated"))
}
}).recover({
case err:Throwable=>
logger.error(s"Could not update status of commission $commissionId to ${requiredUpdate.status}: ", err)
InternalServerError(Json.obj("status"->"db_error","detail"->"Database error, see logs for details"))
})
)
}
def selectIdOfProject(requestedId: Int):Future[Try[Seq[ProjectEntry]]] = db.run(
TableQuery[ProjectEntryRow].filter(_.id === requestedId).result.asTry
)
def doUpdateGenericSelector[T](requestedId:T, selector:T=>Future[Try[Seq[ProjectEntry]]])(f: ProjectEntry=>Future[Try[Int]]):Future[Seq[Try[Int]]] = selector(requestedId).flatMap({
case Success(someSeq)=>
if(someSeq.isEmpty)
Future(Seq(Failure(new RecordNotFoundException(s"No records found for id. $requestedId"))))
else
Future.sequence(someSeq.map(f))
case Failure(error)=>Future(Seq(Failure(error)))
})
def doUpdateGeneric(requestedId:Int)(f: ProjectEntry=>Future[Try[Int]]) = doUpdateGenericSelector[Int](requestedId,selectIdOfProject)(f)
private def internalUpdate(id:Int, request: Request[JsValue]) =
this.validate(request).fold(
errors=>Future(BadRequest(Json.obj("status"->"error","detail"->JsError.toJson(errors)))),
validRecord=>
this.dbupdate(id,validRecord) map {
case Success(rowsUpdated)=> {
ProjectEntry.forCommission(id).map(projects => {
projects.map(project => {
doUpdateGeneric(project.id.get) {record=>
val updatedProjectEntry = record.copy (confidential = validRecord.confidential)
db.run (
TableQuery[ProjectEntryRow].filter (_.id === project.id).update (updatedProjectEntry).asTry
)
}
})
})
Ok(Json.obj("status" -> "ok", "detail" -> "Record updated", "id" -> id))
}
case Failure(error)=>InternalServerError(Json.obj("status"->"error", "detail"->error.toString))
}
)
override def updateByAnyone(id: Int) = IsAuthenticatedAsync(parse.json) { uid=> request=>
internalUpdate(id, request)
}
}