add bisview

This commit is contained in:
Evgenii Alekseev 2019-10-17 01:21:55 +03:00
parent 49fd33fffc
commit eea2f1b04b
30 changed files with 628 additions and 78 deletions

View File

@ -9,14 +9,15 @@ scalacOptions ++= Seq("-deprecation", "-feature")
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.10"
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.10"
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.23"
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.0.4"
libraryDependencies += "javax.ws.rs" % "javax.ws.rs-api" % "2.1.1"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.7.0"
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.3.2"
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2"
libraryDependencies += "org.flywaydb" % "flyway-core" % "6.0.6"

View File

View File

@ -0,0 +1,277 @@
/* in-text images */
figure.img {
float: right;
border: 0px solid #333;
padding: 0px;
margin: 5px 0px 5px 10px;
}
figure.img img {
max-width: 100%;
height: auto;
}
figure.img figcaption {
margin: 0px;
font-size: 90%;
font-style: italic;
text-align: center;
}
h1 .octicon-link, h2 .octicon-link, h3 .octicon-link, h4 .octicon-link, h5 .octicon-link, h6 .octicon-link {
display: none;
color: #222222;
vertical-align: middle;
}
h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor{
padding-left: 8px;
margin-left: -24px;
text-decoration: none;
}
h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link {
display: inline-block;
}
body {
padding: 50px;
font: 14px/1.5 "Liberation Sans", Helvetica, Arial, sans-serif;
color: #555555;
background: #eaeaea
}
h1, h2, h3, h4, h5, h6 {
color: #222222;
margin: 0 0 20px;
}
p, ul, ol, table, pre, dl {
margin: 0 0 20px;
text-align: justify;
}
h1, h2, h3 {
line-height: 1.1;
}
h1 {
font-size: 28px;
}
h2 {
color: #393939;
}
h3, h4, h5, h6 {
color: #494949;
}
a {
color: #3399cc;
font-weight: 350;
text-decoration: none;
}
a small {
font-size: 11px;
color: #777777;
margin-top: -0.6em;
display: block;
}
.wrapper {
width: 80%;
margin: 0 auto;
}
blockquote {
border-left: 1px solid #ffffff;
margin: 0;
padding: 0 0 0 20px;
font-style: italic;
}
code, pre {
font-family: "Liberation Mono", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;
color: #222222;
font-size: 12px;
}
pre {
padding: 8px 15px;
border-radius: 5px;
border: 1px solid #e5e5e5;
overflow-x: auto;
overflow-y: auto;
}
input, select{
box-sizing: border-box;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 5px 10px;
border-bottom: 1px solid #ffffff;
}
td {
text-align: justify;
}
dt {
color: #444444;
font-weight: 700;
}
th {
text-align: left;
color: #444444;
}
img {
max-width: 100%;
}
header {
width: 20%;
float: left;
position: fixed;
}
header ul {
list-style: none;
height: 40px;
padding: 0;
background: #eeeeee;
border-radius: 5px;
border: 1px solid #d2d2d2;
box-shadow: inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0;
width: 15%;
}
header li {
width: 8%;
float: left;
border-right: 1px solid #d2d2d2;
height: 40px;
}
header ul a {
line-height: 1;
font-size: 11px;
color: #999999;
display: block;
text-align: center;
padding-top: 6px;
height: 40px;
}
strong {
color: #222222;
font-weight: 700;
}
header ul li + li {
width: 8%;
border-left: 1px solid #ffffff;
}
header ul li + li + li {
width: 8%;
border-right: none;
}
header ul a strong {
font-size: 14px;
display: block;
color: #222222;
}
section {
width: 70%;
float: right;
padding-bottom: 50px;
}
small {
font-size: 11px;
}
hr {
border: 0;
background: #ffffff;
height: 1px;
margin: 0 0 20px;
}
footer {
width: 20%;
float: left;
position: fixed;
bottom: 50px;
}
@media print, screen and (max-width: 960px) {
div.wrapper {
width: auto;
margin: 0;
}
header, section, footer {
float: none;
position: static;
width: auto;
}
header {
padding-right: 320px;
}
section {
border: 1px solid #e5e5e5;
border-width: 1px 0;
padding: 20px 0;
margin: 0 0 20px;
}
header a small {
display: inline;
}
header ul {
position: absolute;
right: 50px;
top: 52px;
}
}
@media print, screen and (max-width: 720px) {
body {
word-wrap: break-word;
}
header {
padding: 0;
}
header ul, header p.view {
position: static;
}
pre, code {
word-wrap: normal;
}
}
@media print, screen and (max-width: 480px) {
body {
padding: 15px;
}
header ul {
display: none;
}
}
@media print {
body {
padding: 0.4in;
font-size: 12pt;
color: #444444;
}
}

View File

@ -0,0 +1,31 @@
function downloadCsv(csv, filename) {
var csvFile = new Blob([csv], {"type": "text/csv"});
var downloadLink = document.createElement("a");
downloadLink.download = filename;
downloadLink.href = window.URL.createObjectURL(csvFile);
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
}
function exportTableToCsv(filename) {
var table = document.getElementById("result");
var rows = table.getElementsByTagName("tr");
var csv = [];
for (var i = 0; i < rows.length; i++) {
if (rows[i].style.display === "none")
continue
var cols = rows[i].querySelectorAll("td, th");
var row = [];
for (var j = 0; j < cols.length; j++)
row.push(cols[j].innerText);
csv.push(row.join(","));
}
downloadCsv(csv.join("\n"), filename);
}

View File

@ -0,0 +1,21 @@
function searchTable() {
var input = document.getElementById("search");
var filter = input.value.toLowerCase();
var table = document.getElementById("result");
var tr = table.getElementsByTagName("tr");
// from 1 coz of header
for (var i = 1; i < tr.length; i++) {
var td = tr[i].getElementsByClassName("include_search");
var display = "none";
for (var j = 0; j < td.length; j++) {
if (td[j].tagName.toLowerCase() === "td") {
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
display = "";
break;
}
}
}
tr[i].style.display = display;
}
}

View File

@ -11,8 +11,8 @@ import scala.concurrent.{ExecutionContext, Future}
class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
def addPieceBiS(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabaseBiSHandler.AddPieceToBis(playerId, piece) }
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseBiSHandler.AddPieceToBis(playerId, piece)).mapTo[Int]
def bis(partyId: String, playerId: Option[PlayerId])
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
@ -23,7 +23,7 @@ class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariy
downloadBiS(link, playerId.job).map(_.pieces.map(addPieceBiS(playerId, _)))
def removePieceBiS(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabaseBiSHandler.RemovePieceFromBiS(playerId, piece) }
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseBiSHandler.RemovePieceFromBiS(playerId, piece)).mapTo[Int]
}

View File

@ -12,16 +12,16 @@ import scala.concurrent.{ExecutionContext, Future}
class LootHelper(storage: ActorRef) {
def addPieceLoot(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabaseLootHandler.AddPieceTo(playerId, piece) }
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseLootHandler.AddPieceTo(playerId, piece)).mapTo[Int]
def loot(partyId: String, playerId: Option[PlayerId])
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
(storage ? DatabaseLootHandler.GetLoot(partyId, playerId)).mapTo[Seq[Player]]
def removePieceLoot(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabaseLootHandler.RemovePieceFrom(playerId, piece) }
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseLootHandler.RemovePieceFrom(playerId, piece)).mapTo[Int]
def suggestPiece(partyId: String, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =

View File

@ -13,15 +13,16 @@ import scala.util.{Failure, Success}
class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
def addPlayer(player: Player)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
Future { storage ! DatabasePartyHandler.AddPlayer(player) }.andThen {
case Success(_) if player.link.isDefined =>
downloadBiS(player.link.get, player.job).map { bis =>
bis.pieces.map(storage ! DatabaseBiSHandler.AddPieceToBis(player.playerId, _))
}.map(_ => ())
case Success(_) => Future.successful(())
case Failure(exception) => Future.failed(exception)
}
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabasePartyHandler.AddPlayer(player)).mapTo[Int].map { res =>
player.link match {
case Some(link) =>
downloadBiS(link, player.job).map { bis =>
bis.pieces.map(storage ? DatabaseBiSHandler.AddPieceToBis(player.playerId, _))
}.map(_ => res)
case None => Future.successful(res)
}
}.flatten
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
@ -32,6 +33,7 @@ class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(a
(storage ? DatabasePartyHandler.GetParty(partyId)).mapTo[Party].map(_.players.values.toSeq)
}
def removePlayer(playerId: PlayerId)(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabasePartyHandler.RemovePlayer(playerId) }
def removePlayer(playerId: PlayerId)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabasePartyHandler.RemovePlayer(playerId)).mapTo[Int]
}

View File

@ -1,12 +1,12 @@
package me.arcanis.ffxivbis.http
import akka.actor.{ActorRef, ActorSystem}
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.ApiV1Endpoint
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.view.RootView
class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
extends StrictLogging {
@ -17,7 +17,8 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
implicit val timeout: Timeout =
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
private val apiV1Endpoint: ApiV1Endpoint = new ApiV1Endpoint(storage, ariyala)
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala)
private val rootView: RootView = new RootView(storage, ariyala)
def route: Route = apiRoute ~ htmlRoute ~ Swagger.routes ~ swaggerUIRoute
@ -25,7 +26,7 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
ignoreTrailingSlash {
pathPrefix("api") {
pathPrefix(Segment) {
case "v1" => apiV1Endpoint.route
case "v1" => rootApiV1Endpoint.route
case _ => reject
}
}
@ -33,9 +34,9 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
private def htmlRoute: Route =
ignoreTrailingSlash {
pathEndOrSingleSlash {
complete(StatusCodes.OK)
}
pathPrefix("static") {
getFromResourceDirectory("static")
} ~ rootView.route
}
private def swaggerUIRoute: Route =

View File

@ -11,8 +11,8 @@ import scala.concurrent.{ExecutionContext, Future}
class UserHelper(storage: ActorRef) {
def addUser(user: User, isHashedPassword: Boolean)
(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabaseUserHandler.InsertUser(user, isHashedPassword) }
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseUserHandler.InsertUser(user, isHashedPassword)).mapTo[Int]
def user(partyId: String, username: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] =
@ -23,6 +23,6 @@ class UserHelper(storage: ActorRef) {
(storage ? DatabaseUserHandler.GetUsers(partyId)).mapTo[Seq[User]]
def removeUser(partyId: String, username: String)
(implicit executionContext: ExecutionContext): Future[Unit] =
Future { storage ! DatabaseUserHandler.DeleteUser(partyId, username) }
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseUserHandler.DeleteUser(partyId, username)).mapTo[Int]
}

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
@ -19,7 +19,6 @@ import me.arcanis.ffxivbis.models.PlayerId
@Path("api/v1")
class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
import spray.json.DefaultJsonProtocol._
def route: Route = createBiS ~ getBiS ~ modifyBiS
@ -49,7 +48,7 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
put {
entity(as[PlayerBiSLinkResponse]) { bisLink =>
val playerId = bisLink.playerId.withPartyId(partyId)
complete(putBiS(playerId, bisLink.link).map(_ => StatusCodes.Created))
complete(putBiS(playerId, bisLink.link).map(_ => (StatusCodes.Created, HttpEntity.Empty)))
}
}
}
@ -122,7 +121,7 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
case ApiAction.add => addPieceBiS(playerId, action.piece.toPiece)
case ApiAction.remove => removePieceBiS(playerId, action.piece.toPiece)
}
result.map(_ => StatusCodes.Accepted)
result.map(_ => (StatusCodes.Accepted, HttpEntity.Empty))
}
}
}

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
@ -19,7 +19,6 @@ import me.arcanis.ffxivbis.models.PlayerId
@Path("api/v1")
class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization with JsonSupport {
import spray.json.DefaultJsonProtocol._
def route: Route = getLoot ~ modifyLoot
@ -89,7 +88,7 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
case ApiAction.add => addPieceLoot(playerId, action.piece.toPiece)
case ApiAction.remove => removePieceLoot(playerId, action.piece.toPiece)
}
result.map(_ => StatusCodes.Accepted)
result.map(_ => (StatusCodes.Accepted, HttpEntity.Empty))
}
}
}

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
@ -19,7 +19,6 @@ import me.arcanis.ffxivbis.models.PlayerId
@Path("api/v1")
class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport {
import spray.json.DefaultJsonProtocol._
def route: Route = getParty ~ modifyParty
@ -88,7 +87,7 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
case ApiAction.add => addPlayer(player)
case ApiAction.remove => removePlayer(player.playerId)
}
result.map(_ => StatusCodes.Accepted)
result.map(_ => (StatusCodes.Accepted, HttpEntity.Empty))
}
}
}

View File

@ -5,7 +5,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
class ApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
private val biSEndpoint = new BiSEndpoint(storage, ariyala)
private val lootEndpoint = new LootEndpoint(storage)

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
@ -19,12 +19,11 @@ import me.arcanis.ffxivbis.models.Permission
@Path("api/v1")
class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) with Authorization with JsonSupport {
import spray.json.DefaultJsonProtocol._
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
@PUT
@Path("party/{partyId}")
@Path("party/{partyId}/create")
@Consumes(value = Array("application/json"))
@Operation(summary = "create new party", description = "Create new party with specified ID",
parameters = Array(
@ -41,13 +40,13 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
tags = Array("party"),
)
def createParty: Route =
path("party" / Segment) { partyId: String =>
path("party" / Segment / "create") { partyId: String =>
extractExecutionContext { implicit executionContext =>
put {
entity(as[UserResponse]) { user =>
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
complete {
addUser(admin, isHashedPassword = false).map(_ => StatusCodes.Created)
addUser(admin, isHashedPassword = false).map(_ => (StatusCodes.Created, HttpEntity.Empty))
}
}
}
@ -81,7 +80,7 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
entity(as[UserResponse]) { user =>
val withPartyId = user.toUser.copy(partyId = partyId)
complete {
addUser(withPartyId, isHashedPassword = false).map(_ => StatusCodes.Created)
addUser(withPartyId, isHashedPassword = false).map(_ => (StatusCodes.Created, HttpEntity.Empty))
}
}
}
@ -111,7 +110,7 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
delete {
complete {
removeUser(partyId, username).map(_ => StatusCodes.Accepted)
removeUser(partyId, username).map(_ => (StatusCodes.Accepted, HttpEntity.Empty))
}
}
}

View File

@ -4,8 +4,7 @@ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import me.arcanis.ffxivbis.models.Permission
import spray.json._
trait JsonSupport extends SprayJsonSupport {
import DefaultJsonProtocol._
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] =
new RootJsonFormat[E#Value] {

View File

@ -0,0 +1,142 @@
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._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization {
def route: Route = getBiS ~ modifyBiS
def getBiS: Route =
path("party" / Segment / "bis") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
complete {
bis(partyId, None).map { players =>
BiSView.template(partyId, players, Piece.available, None)
}.map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
}
def modifyBiS: Route =
path("party" / Segment / "bis") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post {
formFields("player".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybeIsTome, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) {
case _ => redirect(s"/party/$partyId/bis", StatusCodes.Found)
}
}
}
}
}
}
private def modifyBiSCall(partyId: String, player: String,
maybePiece: Option[String], maybeIsTome: Option[String],
maybeLink: Option[String], action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def getPiece(playerId: PlayerId, piece: String) =
Try(Piece(piece, maybeIsTome.isDefined, playerId.job)).toOption
PlayerId(partyId, player) match {
case Some(playerId) => (maybePiece, action, maybeLink) match {
case (Some(piece), "add", _) => getPiece(playerId, piece) match {
case Some(item) => addPieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
case (Some(piece), "remove", _) => getPiece(playerId, piece) match {
case Some(item) => removePieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
case (_, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
case _ => Future.failed(new Error(s"Could not perform $action"))
}
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
}
}
}
object BiSView {
import scalatags.Text.all._
def template(partyId: String, party: Seq[Player], pieces: Seq[String], error: Option[String]): String = {
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:="Best in slot",
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),
body(
h2("best in slot"),
ErrorView.template(error),
SearchLineView.template,
form(action:=s"/party/$partyId/bis", method:="post")(
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)),
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"),
input(name:="add", id:="add", `type`:="submit", value:="add")
),
form(action:="/bis", method:="post")(
select(name:="player", id:="player", title:="player")
(for (player <- party) yield option(player.playerId.toString)),
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
input(name:="action", id:="action", `type`:="hidden", value:="create"),
input(name:="add", id:="add", `type`:="submit", value:="add")
),
table(
tr(
th("player"),
th("piece"),
th("is tome"),
th("")
//td(`class`:="include_search")
),
for (player <- party; piece <- player.bis.pieces) yield tr(
td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(piece.piece),
td(piece.isTomeToString),
td(
form(action:=s"/party/$partyId/bis", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
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")
)
)
}
}

View File

@ -0,0 +1,11 @@
package me.arcanis.ffxivbis.http.view
import scalatags.Text
import scalatags.Text.all._
object ErrorView {
def template(error: Option[String]): Text.TypedTag[String] = error match {
case Some(text) => p(id:="error", s"Error occurs: $text")
case None => p("")
}
}

View File

@ -0,0 +1,12 @@
package me.arcanis.ffxivbis.http.view
import scalatags.Text
import scalatags.Text.all._
object ExportToCSVView {
def template: Text.TypedTag[String] =
div(
button(onclick:="exportTableToCsv('result.csv')")("Export to CSV"),
script(src:="/static/table_export.js", `type`:="text/javascript")
)
}

View File

@ -0,0 +1,20 @@
package me.arcanis.ffxivbis.http.view
import akka.actor.ActorRef
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
class RootView(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
private val biSView = new BiSView(storage, ariyala)
def route: Route =
biSView.route
}
object RootView {
def toHtml(template: String): HttpEntity.Strict =
HttpEntity(ContentTypes.`text/html(UTF-8)`, template)
}

View File

@ -0,0 +1,14 @@
package me.arcanis.ffxivbis.http.view
import scalatags.Text
import scalatags.Text.all._
object SearchLineView {
def template: Text.TypedTag[String] =
div(
input(
`type`:="text", id:="search", onkeyup:="searchTable()",
placeholder:="search for data", title:="search"
)
)
}

View File

@ -5,6 +5,7 @@ trait Piece {
def job: Job.Job
def piece: String
def isTomeToString: String = if (isTome) "yes" else "no"
def upgrade: Option[PieceUpgrade] = this match {
case _ if !isTome => None
case _: Waist => Some(AccessoryUpgrade)
@ -94,4 +95,8 @@ object Piece {
case "weapon upgrade" => WeaponUpgrade
case other => throw new Error(s"Unknown item type $other")
}
def available: Seq[String] = Seq("weapon",
"head", "body", "hands", "waist", "legs", "feet",
"ears", "neck", "wrist", "leftRing", "rightRing")
}

View File

@ -1,5 +1,7 @@
package me.arcanis.ffxivbis.models
import scala.util.matching.Regex
trait PlayerIdBase {
def job: Job.Job
def nick: String
@ -15,4 +17,10 @@ object PlayerId {
case (Some(nick), Some(job)) => Some(PlayerId(partyId, Job.fromString(job), nick))
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 _ => None
}
}

View File

@ -9,7 +9,8 @@ trait DatabaseBiSHandler { this: Database =>
def bisHandler: Receive = {
case AddPieceToBis(playerId, piece) =>
profile.insertPieceBiS(playerId, piece)
val client = sender()
profile.insertPieceBiS(playerId, piece).pipeTo(client)
case GetBiS(partyId, maybePlayerId) =>
val client = sender()
@ -18,7 +19,8 @@ trait DatabaseBiSHandler { this: Database =>
.pipeTo(client)
case RemovePieceFromBiS(playerId, piece) =>
profile.deletePieceBiS(playerId, piece)
val client = sender()
profile.deletePieceBiS(playerId, piece).pipeTo(client)
}
}

View File

@ -9,7 +9,8 @@ trait DatabaseLootHandler { this: Database =>
def lootHandler: Receive = {
case AddPieceTo(playerId, piece) =>
profile.insertPiece(playerId, piece)
val client = sender()
profile.insertPiece(playerId, piece).pipeTo(client)
case GetLoot(partyId, maybePlayerId) =>
val client = sender()
@ -18,7 +19,8 @@ trait DatabaseLootHandler { this: Database =>
.pipeTo(client)
case RemovePieceFrom(playerId, piece) =>
profile.deletePiece(playerId, piece)
val client = sender()
profile.deletePiece(playerId, piece).pipeTo(client)
case SuggestLoot(partyId, piece) =>
val client = sender()

View File

@ -11,7 +11,8 @@ trait DatabasePartyHandler { this: Actor with StrictLogging with Database =>
def partyHandler: Receive = {
case AddPlayer(player) =>
profile.insertPlayer(player)
val client = sender()
profile.insertPlayer(player).pipeTo(client)
case GetParty(partyId) =>
val client = sender()
@ -27,7 +28,8 @@ trait DatabasePartyHandler { this: Actor with StrictLogging with Database =>
player.pipeTo(client)
case RemovePlayer(playerId) =>
profile.deletePlayer(playerId)
val client = sender()
profile.deletePlayer(playerId).pipeTo(client)
}
}

View File

@ -9,7 +9,8 @@ trait DatabaseUserHandler { this: Database =>
def userHandler: Receive = {
case DeleteUser(partyId, username) =>
profile.deleteUser(partyId, username)
val client = sender()
profile.deleteUser(partyId, username).pipeTo(client)
case GetUser(partyId, username) =>
val client = sender()
@ -20,8 +21,9 @@ trait DatabaseUserHandler { this: Database =>
profile.getUsers(partyId).pipeTo(client)
case InsertUser(user, isHashedPassword) =>
val client = sender()
val toInsert = if (isHashedPassword) user else user.copy(password = user.hash)
profile.insertUser(toInsert)
profile.insertUser(toInsert).pipeTo(client)
}
}

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import slick.lifted.{ForeignKeyQuery, Index}
import slick.lifted.{ForeignKeyQuery, Index, PrimaryKey}
import scala.concurrent.Future
@ -18,9 +18,9 @@ trait BiSProfile { this: DatabaseProfile =>
}
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
def playerId: Rep[Long] = column[Long]("player_id")
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece")
def piece: Rep[String] = column[String]("piece", O.PrimaryKey)
def isTome: Rep[Int] = column[Int]("is_tome")
def job: Rep[String] = column[String]("job")
@ -29,8 +29,6 @@ trait BiSProfile { this: DatabaseProfile =>
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
def bisPiecePlayerIdIdx: Index =
index("bis_piece_player_id_idx", (playerId, piece), unique = true)
}
def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] =

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId}
import slick.lifted.Index
import slick.lifted.{Index, PrimaryKey}
import scala.concurrent.Future
@ -14,8 +14,8 @@ trait PlayersProfile { this: DatabaseProfile =>
Player(partyId, Job.fromString(job), nick, BiS(Seq.empty), List.empty, link, priority)
}
object PlayerRep {
def fromPlayer(player: Player): PlayerRep =
PlayerRep(player.partyId, None, DatabaseProfile.now, player.nick,
def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick,
player.job.toString, player.link, player.priority)
}
@ -30,11 +30,9 @@ trait PlayersProfile { this: DatabaseProfile =>
def * =
(partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply)
def playersNickJobIdx: Index =
index("players_nick_job_idx", (partyId, nick, job), unique = true)
}
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
def getParty(partyId: String): Future[Map[Long, Player]] =
db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) {
@ -45,8 +43,11 @@ trait PlayersProfile { this: DatabaseProfile =>
db.run(player(playerId).map(_.playerId).result.headOption)
def getPlayers(partyId: String): Future[Seq[Long]] =
db.run(players(partyId).map(_.playerId).result)
def insertPlayer(player: Player): Future[Int] =
db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(player)))
def insertPlayer(playerObj: Player): Future[Int] =
getPlayer(playerObj.playerId).map {
case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id))))
case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None)))
}.flatten
private def player(playerId: PlayerId) =
playersTable

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.{Permission, User}
import slick.lifted.Index
import slick.lifted.{Index, PrimaryKey}
import scala.concurrent.Future
@ -13,7 +13,7 @@ trait UsersProfile { this: DatabaseProfile =>
def toUser: User = User(partyId, username, password, Permission.withName(permission))
}
object UserRep {
def fromUser(user: User): UserRep =
def fromUser(user: User, id: Option[Long]): UserRep =
UserRep(user.partyId, None, user.username, user.password, user.permission.toString)
}
@ -27,6 +27,7 @@ trait UsersProfile { this: DatabaseProfile =>
def * =
(partyId, userId.?, username, password, permission) <> ((UserRep.apply _).tupled, UserRep.unapply)
def pk: PrimaryKey = primaryKey("users_username_idx", (partyId, username))
def usersUsernameIdx: Index =
index("users_username_idx", (partyId, username), unique = true)
}
@ -37,9 +38,11 @@ trait UsersProfile { this: DatabaseProfile =>
db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser))
def getUsers(partyId: String): Future[Seq[User]] =
db.run(user(partyId, None).result).map(_.map(_.toUser))
def insertUser(user: User): Future[Int] = {
db.run(usersTable.insertOrUpdate(UserRep.fromUser(user)))
}
def insertUser(userObj: User): Future[Int] =
db.run(user(userObj.partyId, Some(userObj.username)).result.headOption).map {
case Some(user) => db.run(usersTable.update(UserRep.fromUser(userObj, user.userId)))
case _ => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, None)))
}.flatten
private def user(partyId: String, username: Option[String]) =
usersTable