demo with optional loot support

This commit is contained in:
Evgenii Alekseev 2020-03-17 02:13:21 +03:00
parent 10c107d2c2
commit 0171b229a1
19 changed files with 112 additions and 65 deletions

View File

@ -0,0 +1 @@
alter table loot add column is_free_loot integer not null default 0;

View File

@ -0,0 +1,20 @@
alter table loot add column is_free_loot integer;
update loot set is_free_loot = 0;
create table loot_new (
loot_id integer primary key autoincrement,
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
is_free_loot integer not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into loot_new select loot_id, player_id, created, piece, piece_type, job, is_free_loot from loot;
drop index loot_owner_idx;
drop table loot;
alter table loot_new rename to loot;
create index loot_owner_idx on loot(player_id);

View File

@ -22,15 +22,16 @@ trait LootHelper {
def storage: ActorRef def storage: ActorRef
def addPieceLoot(playerId: PlayerId, piece: Piece) def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseLootHandler.AddPieceTo(playerId, piece)).mapTo[Int] (storage ? DatabaseLootHandler.AddPieceTo(playerId, piece, isFreeLoot)).mapTo[Int]
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece) def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece, maybeFree: Option[Boolean])
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
action match { (action, maybeFree) match {
case ApiAction.add => addPieceLoot(playerId, piece) case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot)
case ApiAction.remove => removePieceLoot(playerId, piece) case (ApiAction.remove, _) => removePieceLoot(playerId, piece)
case _ => throw new IllegalArgumentException(s"Invalid combinantion of action $action and fee loot $maybeFree")
} }
def loot(partyId: String, playerId: Option[PlayerId]) def loot(partyId: String, playerId: Option[PlayerId])

View File

@ -103,7 +103,7 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
post { post {
entity(as[PieceActionResponse]) { action => entity(as[PieceActionResponse]) { action =>
val playerId = action.playerId.withPartyId(partyId) val playerId = action.playerId.withPartyId(partyId)
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) { onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception case Failure(exception) => throw exception
} }

View File

@ -41,12 +41,12 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply) implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply) implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat2(LootResponse.apply) implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply)
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2(PartyDescriptionResponse.apply) implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2(PartyDescriptionResponse.apply)
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply) implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply) implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply) implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat3(PieceActionResponse.apply) implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply)
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply) implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] = implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
jsonFormat9(PlayerIdWithCountersResponse.apply) jsonFormat9(PlayerIdWithCountersResponse.apply)

View File

@ -7,11 +7,12 @@ import me.arcanis.ffxivbis.models.Loot
case class LootResponse( case class LootResponse(
@Schema(description = "looted piece", required = true) piece: PieceResponse, @Schema(description = "looted piece", required = true) piece: PieceResponse,
@Schema(description = "loot timestamp", required = true) timestamp: Instant) { @Schema(description = "loot timestamp", required = true) timestamp: Instant,
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp) @Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean) {
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
} }
object LootResponse { object LootResponse {
def fromLoot(loot: Loot): LootResponse = def fromLoot(loot: Loot): LootResponse =
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp) LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot)
} }

View File

@ -13,4 +13,5 @@ import io.swagger.v3.oas.annotations.media.Schema
case class PieceActionResponse( case class PieceActionResponse(
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove")) action: ApiAction.Value, @Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove")) action: ApiAction.Value,
@Schema(description = "piece description", required = true) piece: PieceResponse, @Schema(description = "piece description", required = true) piece: PieceResponse,
@Schema(description = "player description", required = true) playerId: PlayerIdResponse) @Schema(description = "player description", required = true) playerId: PlayerIdResponse,
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean])

View File

@ -30,7 +30,7 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
complete { complete {
val text = LootSuggestView.template(partyId, Seq.empty, None, None) val text = LootSuggestView.template(partyId, Seq.empty, None, false, None)
(StatusCodes.OK, RootView.toHtml(text)) (StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -43,15 +43,18 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post { post {
formFields("piece".as[String], "job".as[String], "piece_type".as[String]) { (piece, job, pieceType) => formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) {
(piece, job, pieceType, maybeFreeLoot) =>
import me.arcanis.ffxivbis.utils.Implicits._
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
onComplete(suggestLootCall(partyId, maybePiece)) { onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) => case Success(players) =>
val text = LootSuggestView.template(partyId, players, maybePiece, None) val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None)
complete(StatusCodes.OK, RootView.toHtml(text)) complete(StatusCodes.OK, RootView.toHtml(text))
case Failure(exception) => case Failure(exception) =>
val text = LootSuggestView.template(partyId, Seq.empty, maybePiece, Some(exception.getMessage)) val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage))
complete(StatusCodes.OK, RootView.toHtml(text)) complete(StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -72,7 +75,8 @@ object LootSuggestView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag} import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String = def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece],
isFreeLoot: Boolean, error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(lang:="en",
head( head(
@ -93,6 +97,8 @@ object LootSuggestView {
(for (job <- Job.availableWithAnyJob) yield option(job.toString)), (for (job <- Job.availableWithAnyJob) yield option(job.toString)),
select(name:="piece_type", id:="piece_type", title:="piece type") select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)), (for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest") input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
), ),
@ -116,6 +122,7 @@ object LootSuggestView {
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")), input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")), input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")),
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=(if (isFreeLoot) "yes" else "no")),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
) )

View File

@ -46,9 +46,9 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String]) { formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String], "free_loot".as[String].?) {
(player, piece, pieceType, action) => (player, piece, pieceType, action, isFreeLoot) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, action)) { _ => onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ =>
redirect(s"/party/$partyId/loot", StatusCodes.Found) redirect(s"/party/$partyId/loot", StatusCodes.Found)
} }
} }
@ -58,14 +58,17 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
} }
private def modifyLootCall(partyId: String, player: String, maybePiece: String, private def modifyLootCall(partyId: String, player: String, maybePiece: String,
maybePieceType: String, action: String) maybePieceType: String, maybeFreeLoot: Option[String],
action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId) = def getPiece(playerId: PlayerId) =
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (getPiece(playerId), action) match { case Some(playerId) => (getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ()) case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot).map(_ => ())
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ()) case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`")) case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
} }
@ -99,6 +102,8 @@ object LootView {
(for (piece <- Piece.available) yield option(piece)), (for (piece <- Piece.available) yield option(piece)),
select(name:="piece_type", id:="piece_type", title:="piece type") select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)), (for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
@ -108,6 +113,7 @@ object LootView {
th("player"), th("player"),
th("piece"), th("piece"),
th("piece type"), th("piece type"),
th("is free loot"),
th("timestamp"), th("timestamp"),
th("") th("")
), ),
@ -115,12 +121,14 @@ object LootView {
td(`class`:="include_search")(player.playerId.toString), td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(loot.piece.piece), td(`class`:="include_search")(loot.piece.piece),
td(loot.piece.pieceType.toString), td(loot.piece.pieceType.toString),
td(loot.isFreeLootToString),
td(loot.timestamp.toString), td(loot.timestamp.toString),
td( td(
form(action:=s"/party/$partyId/loot", method:="post")( form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece), input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString), input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString),
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=loot.isFreeLootToString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name:="remove", id:="remove", `type`:="submit", value:="x")
) )

View File

@ -10,4 +10,6 @@ package me.arcanis.ffxivbis.models
import java.time.Instant import java.time.Instant
case class Loot(playerId: Long, piece: Piece, timestamp: Instant) case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
}

View File

@ -31,10 +31,7 @@ case class Player(id: Long,
def withLoot(piece: Loot): Player = withLoot(Seq(piece)) def withLoot(piece: Loot): Player = withLoot(Seq(piece))
def withLoot(list: Seq[Loot]): Player = { def withLoot(list: Seq[Loot]): Player = {
require(loot.forall(_.playerId == id), "player id must be same") require(loot.forall(_.playerId == id), "player id must be same")
list match { copy(loot = loot ++ list)
case Nil => this
case _ => copy(loot = list)
}
} }
def isRequired(piece: Option[Piece]): Boolean = { def isRequired(piece: Option[Piece]): Boolean = {
@ -48,10 +45,10 @@ case class Player(id: Long,
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage) def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage)
def lootCount(piece: Option[Piece]): Int = piece match { def lootCount(piece: Option[Piece]): Int = piece match {
case Some(p) => loot.count(_.piece == p) case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p)
case None => lootCountTotal(piece) case None => lootCountTotal(piece)
} }
def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece) def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece)
def lootCountTotal(piece: Option[Piece]): Int = loot.length def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot)
def lootPriority(piece: Piece): Int = priority def lootPriority(piece: Piece): Int = priority
} }

View File

@ -8,17 +8,20 @@
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import java.time.Instant
import akka.pattern.pipe import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{Piece, PlayerId} import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.Database
trait DatabaseLootHandler { this: Database => trait DatabaseLootHandler { this: Database =>
import DatabaseLootHandler._ import DatabaseLootHandler._
def lootHandler: Receive = { def lootHandler: Receive = {
case AddPieceTo(playerId, piece) => case AddPieceTo(playerId, piece, isFreeLoot) =>
val client = sender() val client = sender()
profile.insertPiece(playerId, piece).pipeTo(client) val loot = Loot(-1, piece, Instant.now, isFreeLoot)
profile.insertPiece(playerId, loot).pipeTo(client)
case GetLoot(partyId, maybePlayerId) => case GetLoot(partyId, maybePlayerId) =>
val client = sender() val client = sender()
@ -37,7 +40,7 @@ trait DatabaseLootHandler { this: Database =>
} }
object DatabaseLootHandler { object DatabaseLootHandler {
case class AddPieceTo(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest { case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class GetLoot(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest case class GetLoot(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest

View File

@ -22,7 +22,7 @@ trait BiSProfile { this: DatabaseProfile =>
pieceType: String, job: String) { pieceType: String, job: String) {
def toLoot: Loot = Loot( def toLoot: Loot = Loot(
playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)), playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created)) Instant.ofEpochMilli(created), isFreeLoot = false)
} }
object BiSRep { object BiSRep {
def fromPiece(playerId: Long, piece: Piece): BiSRep = def fromPiece(playerId: Long, piece: Piece): BiSRep =

View File

@ -44,14 +44,17 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
byPlayerId(playerId, insertPieceBiSById(piece)) byPlayerId(playerId, insertPieceBiSById(piece))
// generic loot api // generic loot api
def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] = def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] = {
byPlayerId(playerId, deletePieceById(piece)) // we don't really care here about loot
val loot = Loot(-1, piece, Instant.now, isFreeLoot = false)
byPlayerId(playerId, deletePieceById(loot))
}
def getPieces(playerId: PlayerId): Future[Seq[Loot]] = def getPieces(playerId: PlayerId): Future[Seq[Loot]] =
byPlayerId(playerId, getPiecesById) byPlayerId(playerId, getPiecesById)
def getPieces(partyId: String): Future[Seq[Loot]] = def getPieces(partyId: String): Future[Seq[Loot]] =
byPartyId(partyId, getPiecesById) byPartyId(partyId, getPiecesById)
def insertPiece(playerId: PlayerId, piece: Piece): Future[Int] = def insertPiece(playerId: PlayerId, loot: Loot): Future[Int] =
byPlayerId(playerId, insertPieceById(piece)) byPlayerId(playerId, insertPieceById(loot))
private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] =
getPlayers(partyId).map(callback).flatten getPlayers(partyId).map(callback).flatten

View File

@ -19,16 +19,18 @@ trait LootProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class LootRep(lootId: Option[Long], playerId: Long, created: Long, case class LootRep(lootId: Option[Long], playerId: Long, created: Long,
piece: String, pieceType: String, job: String) { piece: String, pieceType: String, job: String,
isFreeLoot: Int) {
def toLoot: Loot = Loot( def toLoot: Loot = Loot(
playerId, playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)), Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created)) Instant.ofEpochMilli(created), isFreeLoot == 1)
} }
object LootRep { object LootRep {
def fromPiece(playerId: Long, piece: Piece): LootRep = def fromLoot(playerId: Long, loot: Loot): LootRep =
LootRep(None, playerId, DatabaseProfile.now, piece.piece, LootRep(None, playerId, loot.timestamp.toEpochMilli, loot.piece.piece,
piece.pieceType.toString, piece.job.toString) loot.piece.pieceType.toString, loot.piece.job.toString,
if (loot.isFreeLoot) 1 else 0)
} }
class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") { class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") {
@ -38,9 +40,10 @@ trait LootProfile { this: DatabaseProfile =>
def piece: Rep[String] = column[String]("piece") def piece: Rep[String] = column[String]("piece")
def pieceType: Rep[String] = column[String]("piece_type") def pieceType: Rep[String] = column[String]("piece_type")
def job: Rep[String] = column[String]("job") def job: Rep[String] = column[String]("job")
def isFreeLoot: Rep[Int] = column[Int]("is_free_loot")
def * = def * =
(lootId.?, playerId, created, piece, pieceType, job) <> ((LootRep.apply _).tupled, LootRep.unapply) (lootId.?, playerId, created, piece, pieceType, job, isFreeLoot) <> ((LootRep.apply _).tupled, LootRep.unapply)
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] = def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade) foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
@ -48,16 +51,16 @@ trait LootProfile { this: DatabaseProfile =>
index("loot_owner_idx", (playerId), unique = false) index("loot_owner_idx", (playerId), unique = false)
} }
def deletePieceById(piece: Piece)(playerId: Long): Future[Int] = def deletePieceById(loot: Loot)(playerId: Long): Future[Int] =
db.run(pieceLoot(LootRep.fromPiece(playerId, piece)).map(_.lootId).max.result).flatMap { db.run(pieceLoot(LootRep.fromLoot(playerId, loot)).map(_.lootId).max.result).flatMap {
case Some(id) => db.run(lootTable.filter(_.lootId === id).delete) case Some(id) => db.run(lootTable.filter(_.lootId === id).delete)
case _ => throw new IllegalArgumentException(s"Could not find piece $piece belong to $playerId") case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId")
} }
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot)) db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot))
def insertPieceById(piece: Piece)(playerId: Long): Future[Int] = def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
db.run(lootTable.insertOrUpdate(LootRep.fromPiece(playerId, piece))) db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot)))
private def pieceLoot(piece: LootRep) = private def pieceLoot(piece: LootRep) =
piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece) piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece)

View File

@ -70,7 +70,7 @@ class BiSEndpointTest extends WordSpec
"remove item from best in slot set" in { "remove item from best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody) val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.remove, piece, playerId) val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted
@ -89,7 +89,7 @@ class BiSEndpointTest extends WordSpec
"add item to best in slot set" in { "add item to best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody) val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.add, piece, playerId) val entity = PieceActionResponse(ApiAction.add, piece, playerId, None)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted

View File

@ -51,7 +51,7 @@ class LootEndpointTest extends WordSpec
"add item to loot" in { "add item to loot" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody) val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.add, piece, playerId) val entity = PieceActionResponse(ApiAction.add, piece, playerId, Some(false))
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted
@ -76,7 +76,7 @@ class LootEndpointTest extends WordSpec
"remove item from loot" in { "remove item from loot" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody) val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.remove, piece, playerId) val entity = PieceActionResponse(ApiAction.remove, piece, playerId, Some(false))
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted

View File

@ -34,7 +34,7 @@ class DatabaseLootHandlerTest
"add loot" in { "add loot" in {
Fixtures.loot.foreach { piece => Fixtures.loot.foreach { piece =>
database ! impl.DatabaseLootHandler.AddPieceTo(Fixtures.playerEmpty.playerId, piece) database ! impl.DatabaseLootHandler.AddPieceTo(Fixtures.playerEmpty.playerId, piece, isFreeLoot = false)
expectMsg(timeout, 1) expectMsg(timeout, 1)
} }
} }
@ -66,11 +66,11 @@ class DatabaseLootHandlerTest
} }
"add same loot" in { "add same loot" in {
database ! impl.DatabaseLootHandler.AddPieceTo(Fixtures.playerEmpty.playerId, Fixtures.lootBody) database ! impl.DatabaseLootHandler.AddPieceTo(Fixtures.playerEmpty.playerId, Fixtures.lootBody, isFreeLoot = false)
expectMsg(timeout, 1) expectMsg(timeout, 1)
Fixtures.loot.foreach { piece => Fixtures.loot.foreach { piece =>
database ! impl.DatabaseLootHandler.AddPieceTo(Fixtures.playerEmpty.playerId, piece) database ! impl.DatabaseLootHandler.AddPieceTo(Fixtures.playerEmpty.playerId, piece, isFreeLoot = false)
expectMsg(timeout, 1) expectMsg(timeout, 1)
} }

View File

@ -7,5 +7,5 @@ import me.arcanis.ffxivbis.models.{Loot, Piece}
import scala.language.implicitConversions import scala.language.implicitConversions
object Converters { object Converters {
implicit def pieceToLoot(piece: Piece): Loot = Loot(-1, piece, Instant.ofEpochMilli(0)) implicit def pieceToLoot(piece: Piece): Loot = Loot(-1, piece, Instant.ofEpochMilli(0), isFreeLoot = false)
} }