From 9668a0edd154fd578195bd8e707db5fed7386dc8 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Thu, 17 Oct 2019 20:08:28 +0300 Subject: [PATCH] base views --- .../arcanis/ffxivbis/http/view/BiSView.scala | 10 +- .../ffxivbis/http/view/LootSuggestView.scala | 120 +++++++++++++++++ .../arcanis/ffxivbis/http/view/LootView.scala | 10 +- .../ffxivbis/http/view/PlayerView.scala | 124 ++++++++++++++++++ .../arcanis/ffxivbis/http/view/RootView.scala | 4 +- .../arcanis/ffxivbis/http/view/UserView.scala | 6 +- .../me/arcanis/ffxivbis/models/PlayerId.scala | 5 +- .../models/PlayerIdWithCounters.scala | 4 +- .../me/arcanis/ffxivbis/utils/Implicits.scala | 5 + 9 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala index 6d971f5..a0aed54 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala @@ -23,7 +23,7 @@ class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeou get { complete { bis(partyId, None).map { players => - BiSView.template(partyId, players, Piece.available, None) + BiSView.template(partyId, players, None) }.map { text => (StatusCodes.OK, RootView.toHtml(text)) } @@ -53,8 +53,10 @@ class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeou maybePiece: Option[String], maybeIsTome: Option[String], maybeLink: Option[String], action: String) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { + import me.arcanis.ffxivbis.utils.Implicits._ + def getPiece(playerId: PlayerId, piece: String) = - Try(Piece(piece, maybeIsTome.isDefined, playerId.job)).toOption + Try(Piece(piece, maybeIsTome, playerId.job)).toOption PlayerId(partyId, player) match { case Some(playerId) => (maybePiece, action, maybeLink) match { @@ -77,7 +79,7 @@ class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeou object BiSView { import scalatags.Text.all._ - def template(partyId: String, party: Seq[Player], pieces: Seq[String], error: Option[String]): String = + def template(partyId: String, party: Seq[Player], error: Option[String]): String = "" + html(lang:="en", head( @@ -95,7 +97,7 @@ object BiSView { select(name:="player", id:="player", title:="player") (for (player <- party) yield option(player.playerId.toString)), select(name:="piece", id:="piece", title:="piece") - (for (piece <- pieces) yield option(piece)), + (for (piece <- Piece.available) yield option(piece)), input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"), label(`for`:="is_tome")("is tome gear"), input(name:="action", id:="action", `type`:="hidden", value:="add"), diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala new file mode 100644 index 0000000..0ec7a54 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala @@ -0,0 +1,120 @@ +package me.arcanis.ffxivbis.http.view + +import akka.actor.ActorRef +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import akka.util.Timeout +import me.arcanis.ffxivbis.http.{Authorization, LootHelper} +import me.arcanis.ffxivbis.models.{Piece, PlayerIdWithCounters} + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} + +class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout) + extends LootHelper(storage) with Authorization { + + def route: Route = getIndex ~ suggestLoot + + def getIndex: Route = + path("party" / Segment / "suggest") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => + get { + complete { + val text = LootSuggestView.template(partyId, Seq.empty, None, None) + (StatusCodes.OK, RootView.toHtml(text)) + } + } + } + } + } + + def suggestLoot: Route = + path("party" / Segment / "suggest") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => + post { + formFields("piece".as[String], "is_tome".as[String].?) { (piece, maybeTome) => + import me.arcanis.ffxivbis.utils.Implicits._ + val maybePiece = Try(Piece(piece, maybeTome)).toOption + + onComplete(suggestLootCall(partyId, maybePiece)) { + case Success(players) => + val text = LootSuggestView.template(partyId, players, maybePiece, None) + complete(StatusCodes.OK, RootView.toHtml(text)) + case Failure(exception) => + val text = LootSuggestView.template(partyId, Seq.empty, maybePiece, Some(exception.getMessage)) + complete(StatusCodes.OK, RootView.toHtml(text)) + } + } + } + } + } + } + + private def suggestLootCall(partyId: String, maybePiece: Option[Piece]) + (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] = + maybePiece match { + case Some(piece) => suggestPiece(partyId, piece) + case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`")) + } +} + +object LootSuggestView { + import scalatags.Text.all._ + + def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String = + "" + + html(lang:="en", + head( + title:="Suggest loot", + link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") + ), + + body( + h2("Suggest loot"), + + ErrorView.template(error), + SearchLineView.template, + + form(action:=s"/party/$partyId/suggest", method:="post")( + select(name:="piece", id:="piece", title:="piece") + (for (piece <- Piece.available) yield option(piece)), + input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"), + label(`for`:="is_tome")("is tome gear"), + input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest") + ), + + table(id:="result")( + tr( + th("player"), + th("is required"), + th("these pieces looted"), + th("total bis pieces looted"), + th("total pieces looted"), + th("") + ), + for (player <- party) yield tr( + td(`class`:="include_search")(player.playerId.toString), + td(player.isRequiredToString), + td(player.lootCount), + td(player.lootCountBiS), + td(player.lootCountTotal), + td( + form(action:=s"/party/$partyId/loot", method:="post")( + 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:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).getOrElse("")), + input(name:="action", id:="action", `type`:="hidden", value:="add"), + input(name:="add", id:="add", `type`:="submit", value:="add") + ) + ) + ) + ), + + ExportToCSVView.template, + script(src:="/static/table_search.js", `type`:="text/javascript") + ) + ) +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala index 744e69b..5264413 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala @@ -23,7 +23,7 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout) get { complete { loot(partyId, None).map { players => - LootView.template(partyId, players, Piece.available, None) + LootView.template(partyId, players, None) }.map { text => (StatusCodes.OK, RootView.toHtml(text)) } @@ -53,8 +53,10 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout) maybePiece: String, maybeIsTome: Option[String], action: String) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { + import me.arcanis.ffxivbis.utils.Implicits._ + def getPiece(playerId: PlayerId) = - Try(Piece(maybePiece, maybeIsTome.isDefined, playerId.job)).toOption + Try(Piece(maybePiece, maybeIsTome, playerId.job)).toOption PlayerId(partyId, player) match { case Some(playerId) => (getPiece(playerId), action) match { @@ -70,7 +72,7 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout) object LootView { import scalatags.Text.all._ - def template(partyId: String, party: Seq[Player], pieces: Seq[String], error: Option[String]): String = + def template(partyId: String, party: Seq[Player], error: Option[String]): String = "" + html(lang:="en", head( @@ -88,7 +90,7 @@ object LootView { select(name:="player", id:="player", title:="player") (for (player <- party) yield option(player.playerId.toString)), select(name:="piece", id:="piece", title:="piece") - (for (piece <- pieces) yield option(piece)), + (for (piece <- Piece.available) yield option(piece)), input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"), label(`for`:="is_tome")("is tome gear"), input(name:="action", id:="action", `type`:="hidden", value:="add"), diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala new file mode 100644 index 0000000..4581908 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala @@ -0,0 +1,124 @@ +package me.arcanis.ffxivbis.http.view + +import akka.actor.ActorRef +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import akka.util.Timeout +import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} +import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId, PlayerIdWithCounters} + +import scala.concurrent.{ExecutionContext, Future} + +class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) + extends PlayerHelper(storage, ariyala) with Authorization { + + def route: Route = getParty ~ modifyParty + + def getParty: Route = + path("party" / Segment) { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => + get { + complete { + getPlayers(partyId, None).map { players => + PlayerView.template(partyId, players.map(_.withCounters(None)), None) + }.map { text => + (StatusCodes.OK, RootView.toHtml(text)) + } + } + } + } + } + } + + def modifyParty: Route = + path("party" / Segment) { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => + post { + formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) { + (nick, job, maybePriority, maybeLink, action) => + onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { + case _ => redirect(s"/party/$partyId", StatusCodes.Found) + } + } + } + } + } + } + + private def modifyPartyCall(partyId: String, nick: String, job: String, + maybePriority: Option[Int], maybeLink: Option[String], + action: String) + (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { + def maybePlayerId = PlayerId(partyId, Some(nick), Some(job)) + def player(playerId: PlayerId) = + Player(partyId, playerId.job, playerId.nick, BiS(), Seq.empty, maybeLink, maybePriority.getOrElse(0)) + + (action, maybePlayerId) match { + case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ()) + case ("remove", Some(playerId)) => removePlayer(playerId).map(_ => ()) + case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)")) + } + } +} + +object PlayerView { + import scalatags.Text.all._ + + def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String = + "" + + html(lang:="en", + head( + title:="Party", + link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") + ), + + body( + h2("Party"), + + ErrorView.template(error), + SearchLineView.template, + + form(action:=s"/party/$partyId", method:="post")( + input(name:="nick", id:="nick", placeholder:="nick", title:="nick", `type`:="nick"), + select(name:="job", id:="job", title:="job") + (for (job <- Job.groupAll) yield option(job.toString)), + input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"), + input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"), + input(name:="action", id:="action", `type`:="hidden", value:="add"), + input(name:="add", id:="add", `type`:="submit", value:="add") + ), + + table(id:="result")( + tr( + th("nick"), + th("job"), + th("total bis pieces looted"), + th("total pieces looted"), + th("priority"), + th("") + ), + for (player <- party) yield tr( + td(`class`:="include_search")(player.nick), + td(`class`:="include_search")(player.job.toString), + td(player.lootCountBiS), + td(player.lootCountTotal), + td(player.priority), + td( + form(action:=s"/party/$partyId", method:="post")( + input(name:="nick", id:="nick", `type`:="hidden", value:=player.nick), + input(name:="job", id:="job", `type`:="hidden", value:=player.job.toString), + input(name:="action", id:="action", `type`:="hidden", value:="remove"), + input(name:="remove", id:="remove", `type`:="submit", value:="x") + ) + ) + ) + ), + + ExportToCSVView.template, + script(src:="/static/table_search.js", `type`:="text/javascript") + ) + ) +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala index 8745847..4e4e47a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala @@ -10,10 +10,12 @@ class RootView(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) private val biSView = new BiSView(storage, ariyala) private val lootView = new LootView(storage) + private val lootSuggestView = new LootSuggestView(storage) + private val playerView = new PlayerView(storage, ariyala) private val userView = new UserView(storage) def route: Route = - biSView.route ~ lootView.route ~ userView.route + biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route } object RootView { diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala index 3db6e01..087e9e0 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala @@ -14,7 +14,7 @@ import scala.util.Try class UserView(override val storage: ActorRef)(implicit timeout: Timeout) extends UserHelper(storage) with Authorization { - def route: Route = getUsers + def route: Route = getUsers ~ modifyUsers def getUsers: Route = path("party" / Segment / "users") { partyId: String => @@ -85,8 +85,8 @@ object UserView { SearchLineView.template, form(action:=s"/party/$partyId/users", method:="post")( - input(name:="username", id:="username", title:="username", placeholder:="username", `type`:="text"), - input(name:="password", id:="password", title:="password", placeholder:="password", `type`:="password"), + input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"), + input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"), select(name:="permission", id:="permission", title:="permission")(option("get"), option("post")), input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="add", id:="add", `type`:="submit", value:="add") diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala index e6f1d5d..ad70525 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala @@ -1,5 +1,6 @@ package me.arcanis.ffxivbis.models +import scala.util.Try import scala.util.matching.Regex trait PlayerIdBase { @@ -14,13 +15,13 @@ case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerI object PlayerId { def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] = (maybeNick, maybeJob) match { - case (Some(nick), Some(job)) => Some(PlayerId(partyId, Job.fromString(job), nick)) + case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.fromString(job), nick)).toOption case _ => None } private val prettyPlayerIdRegex: Regex = "^(.*) \\(([A-Z]{3})\\)$".r def apply(partyId: String, player: String): Option[PlayerId] = player match { - case s"${prettyPlayerIdRegex(nick, job)}" => Some(PlayerId(partyId, Job.fromString(job), nick)) + case s"${prettyPlayerIdRegex(nick, job)}" => Try(PlayerId(partyId, Job.fromString(job), nick)).toOption case _ => None } } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala index 0761f60..9c8fa14 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala @@ -12,10 +12,10 @@ case class PlayerIdWithCounters(partyId: String, extends PlayerIdBase { import PlayerIdWithCounters._ - def playerId: PlayerId = PlayerId(partyId, job, nick) - def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean = withCounters(orderBy) > that.withCounters(orderBy) + def isRequiredToString: String = if (isRequired) "yes" else "no" + def playerId: PlayerId = PlayerId(partyId, job, nick) private val counters: Map[String, Int] = Map( "isRequired" -> (if (isRequired) 1 else 0), diff --git a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala index 636bb28..054613e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala +++ b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala @@ -9,6 +9,11 @@ import scala.concurrent.duration.FiniteDuration import scala.language.implicitConversions object Implicits { + implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes match { + case Some("yes" | "on") => true + case _ => false + } + implicit def getFiniteDuration(duration: Duration): Timeout = FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS) }