some html implementation

This commit is contained in:
2019-09-08 03:06:31 +03:00
parent 52a3c7fee7
commit 9bd51d9267
18 changed files with 425 additions and 13 deletions

View File

@ -11,9 +11,13 @@ from aiohttp.web import Application
from service.api.views.api.bis import BiSView from service.api.views.api.bis import BiSView
from service.api.views.api.loot import LootView from service.api.views.api.loot import LootView
from service.api.views.api.player import PlayerView from service.api.views.api.player import PlayerView
from service.api.views.html.bis import BiSHtmlView
from service.api.views.html.index import IndexHtmlView
from service.api.views.html.player import PlayerHtmlView
def setup_routes(app: Application) -> None: def setup_routes(app: Application) -> None:
# api routes
app.router.add_get('/api/v1/party', PlayerView) app.router.add_get('/api/v1/party', PlayerView)
app.router.add_post('/api/v1/party', PlayerView) app.router.add_post('/api/v1/party', PlayerView)
@ -23,4 +27,13 @@ def setup_routes(app: Application) -> None:
app.router.add_get('/api/v1/party/loot', LootView) app.router.add_get('/api/v1/party/loot', LootView)
app.router.add_post('/api/v1/party/loot', LootView) app.router.add_post('/api/v1/party/loot', LootView)
app.router.add_put('/api/v1/party/loot', LootView) app.router.add_put('/api/v1/party/loot', LootView)
# html routes
app.router.add_get('/', IndexHtmlView)
app.router.add_get('/party', PlayerHtmlView)
app.router.add_post('/party', PlayerHtmlView)
app.router.add_get('/bis', BiSHtmlView)
app.router.add_post('/bis', BiSHtmlView)

View File

@ -21,7 +21,7 @@ class PlayerView(PlayerBaseView):
party = self.player_get(self.request.query.getone('nick', None)) party = self.player_get(self.request.query.getone('nick', None))
except Exception as e: except Exception as e:
self.request.app.logger.exception('could not get loot') self.request.app.logger.exception('could not get party')
return wrap_exception(e, self.request.query) return wrap_exception(e, self.request.query)
return wrap_json(party, self.request.query) return wrap_json(party, self.request.query)

View File

@ -22,7 +22,7 @@ class PlayerBaseView(View):
player_id = player.player_id player_id = player.player_id
self.request.app['party'].set_player(player) self.request.app['party'].set_player(player)
if link is not None: if link:
parser = AriyalaParser(self.request.app['config']) parser = AriyalaParser(self.request.app['config'])
items = parser.get(link) items = parser.get(link)
for piece in items: for piece in items:

View File

@ -0,0 +1,80 @@
#
# Copyright (c) 2019 Evgeniy Alekseev.
#
# This file is part of ffxivbis
# (see https://github.com/arcan1s/ffxivbis).
#
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import HTTPFound, Response
from aiohttp_jinja2 import template
from typing import Any, Dict, List
from service.models.job import Job
from service.models.piece import Piece
from service.models.player import PlayerId
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.bis_base import BiSBaseView
from service.api.views.common.player_base import PlayerBaseView
class BiSHtmlView(BiSBaseView, PlayerBaseView):
@template('bis.jinja2')
async def get(self) -> Dict[str, Any]:
items: List[Dict[str, str]] = []
error = None
try:
players = self.player_get(None)
items = [
{
'player': player.player_id.pretty_name,
'piece': piece.name,
'is_tome': 'yes' if piece.is_tome else 'no'
}
for player in players
for piece in player.bis.pieces
]
except Exception as e:
self.request.app.logger.exception('could not get bis')
error = repr(e)
return {
'pieces': Piece.available(),
'players': items,
'request_error': error
}
async def post(self) -> Response:
data = await self.request.post()
required = ['method', 'player']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)
try:
method = data.get('method')
player_id = PlayerId.from_pretty_name(data.get('player'))
if method == 'post':
required = ['action', 'piece']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)
self.bis_post(data.get('action'), player_id,
Piece.get({'piece': data.get('piece'), 'is_tome': data.get('is_tome', False)}))
elif method == 'put':
required = ['bis']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)
self.bis_put(player_id, data.get('bis'))
except Exception as e:
self.request.app.logger.exception('could not manage bis')
return wrap_exception(e, data)
return HTTPFound(self.request.url)

View File

@ -0,0 +1,18 @@
#
# Copyright (c) 2019 Evgeniy Alekseev.
#
# This file is part of ffxivbis
# (see https://github.com/arcan1s/ffxivbis).
#
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import View
from aiohttp_jinja2 import template
from typing import Any, Dict
class IndexHtmlView(View):
@template('index.jinja2')
async def get(self) -> Dict[str, Any]:
return {}

View File

@ -0,0 +1,66 @@
#
# Copyright (c) 2019 Evgeniy Alekseev.
#
# This file is part of ffxivbis
# (see https://github.com/arcan1s/ffxivbis).
#
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import HTTPFound, Response
from aiohttp_jinja2 import template
from typing import Any, Dict, List
from service.models.job import Job
from service.models.player import PlayerIdWithCounters
from service.api.utils import wrap_exception, wrap_invalid_param
from service.api.views.common.player_base import PlayerBaseView
class PlayerHtmlView(PlayerBaseView):
@template('party.jinja2')
async def get(self) -> Dict[str, Any]:
counters: List[PlayerIdWithCounters] = []
error = None
try:
party = self.player_get(None)
counters = [player.player_id_with_counters(None) for player in party]
except Exception as e:
self.request.app.logger.exception('could not get party')
error = repr(e)
return {
'players': [
{
'job': player.job.name,
'nick': player.nick,
'loot_count_bis': player.loot_count_bis,
'loot_count': player.loot_count,
'priority': player.priority
}
for player in counters
],
'request_error': error
}
async def post(self) -> Response:
data = await self.request.post()
required = ['action', 'job', 'nick']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)
try:
action = data.get('action')
priority = data.get('priority', 0)
link = data.get('bis', None)
self.player_post(action, Job[data['job'].upper()], data['nick'], link, priority)
except Exception as e:
self.request.app.logger.exception('could not manage players')
return wrap_exception(e, data)
return HTTPFound(self.request.url)

View File

@ -6,6 +6,8 @@
# #
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause # License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
# #
import aiohttp_jinja2
import jinja2
import logging import logging
from aiohttp import web from aiohttp import web
@ -38,6 +40,8 @@ def setup_service(config: Configuration, database: Database, loot: LootSelector,
# routes # routes
app.logger.info('setup routes') app.logger.info('setup routes')
setup_routes(app) setup_routes(app)
if config.has_option('web', 'templates'):
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(config.get('web', 'templates')))
app.logger.info('setup configuration') app.logger.info('setup configuration')
app['config'] = config app['config'] = config

View File

@ -9,7 +9,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Mapping, Type, Union from typing import Any, List, Mapping, Type, Union
from .upgrade import Upgrade from .upgrade import Upgrade
@ -33,11 +33,19 @@ class Piece:
return Upgrade.GearUpgrade return Upgrade.GearUpgrade
return Upgrade.NoUpgrade return Upgrade.NoUpgrade
@staticmethod
def available() -> List[str]:
return [
'weapon',
'head', 'body', 'hands', 'waist', 'legs', 'feet',
'ears', 'neck', 'wrist', 'left_ring', 'right_ring'
]
@classmethod @classmethod
def get(cls: Type[Piece], data: Mapping[str, Any]) -> Union[Piece, Upgrade]: def get(cls: Type[Piece], data: Mapping[str, Any]) -> Union[Piece, Upgrade]:
try: try:
piece_type = data['piece'] piece_type = data['piece']
is_tome = bool(data['is_tome']) is_tome = data['is_tome'] in ('yes', 'on', '1', 1, True)
except KeyError: except KeyError:
raise InvalidDataRow(data) raise InvalidDataRow(data)
if piece_type.lower() == 'weapon': if piece_type.lower() == 'weapon':

View File

@ -6,8 +6,12 @@
# #
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause # License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
# #
from __future__ import annotations
import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Union from typing import List, Optional, Type, Union
from .bis import BiS from .bis import BiS
from .job import Job from .job import Job
@ -20,12 +24,22 @@ class PlayerId:
job: Job job: Job
nick: str nick: str
@property
def pretty_name(self) -> str:
return '{} ({})'.format(self.nick, self.job.name)
@classmethod
def from_pretty_name(cls: Type[PlayerId], value: str) -> PlayerId:
matches = re.search('^(?P<nick>.*) \((?P<job>[A-Z]+)\)$', value)
return PlayerId(Job[matches.group('job')], matches.group('nick'))
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(str(self)) return hash(str(self))
@dataclass @dataclass
class PlayerIdWithCounters(PlayerId): class PlayerIdWithCounters(PlayerId):
priority: int
loot_count: int loot_count: int
loot_count_bis: int loot_count_bis: int
loot_count_total: int loot_count_total: int
@ -50,12 +64,15 @@ class Player:
def player_id(self) -> PlayerId: def player_id(self) -> PlayerId:
return PlayerId(self.job, self.nick) return PlayerId(self.job, self.nick)
def player_id_with_counters(self, piece: Union[Piece, Upgrade]) -> PlayerIdWithCounters: def player_id_with_counters(self, piece: Union[Piece, Upgrade, None]) -> PlayerIdWithCounters:
return PlayerIdWithCounters(self.job, self.nick, self.loot_count(piece), return PlayerIdWithCounters(self.job, self.nick, self.priority, self.loot_count(piece),
self.loot_count_bis(piece), self.loot_count_total(piece)) self.loot_count_bis(piece), self.loot_count_total(piece))
# ordering methods # ordering methods
def is_required(self, piece: Union[Piece, Upgrade]) -> bool: def is_required(self, piece: Union[Piece, Upgrade, None]) -> bool:
if piece is None:
return False
# lets check if it is even in bis # lets check if it is even in bis
if not self.bis.has_piece(piece): if not self.bis.has_piece(piece):
return False return False
@ -68,14 +85,16 @@ class Player:
return self.bis.upgrades_required[piece] > self.loot_count(piece) return self.bis.upgrades_required[piece] > self.loot_count(piece)
return False return False
def loot_count(self, piece: Union[Piece, Upgrade]) -> int: def loot_count(self, piece: Union[Piece, Upgrade, None]) -> int:
if piece is None:
return len(self.loot)
return self.loot.count(piece) return self.loot.count(piece)
def loot_count_bis(self, _: Union[Piece, Upgrade]) -> int: def loot_count_bis(self, _: Union[Piece, Upgrade, None]) -> int:
return len([piece for piece in self.loot if self.bis.has_piece(piece)]) return len([piece for piece in self.loot if self.bis.has_piece(piece)])
def loot_count_total(self, _: Union[Piece, Upgrade]) -> int: def loot_count_total(self, _: Union[Piece, Upgrade, None]) -> int:
return len(self.loot) return len(self.loot)
def loot_priority(self, _: Union[Piece, Upgrade]) -> int: def loot_priority(self, _: Union[Piece, Upgrade, None]) -> int:
return self.priority return self.priority

67
templates/bis.jinja2 Normal file
View File

@ -0,0 +1,67 @@
<html lang="en">
<head>
<title>Best in slot</title>
{% include "style.jinja2" %}
</head>
<body>
<h2>best in slot</h2>
{% include "error.jinja2" %}
{% include "search_line.jinja2" %}
<form action="/bis" method="post">
<select name="player" id="player" title="player">
{% for player in players %}
<option>{{ player.player|e }}</option>
{% endfor %}
</select>
<select name="piece" id="piece" title="piece">
{% for piece in pieces %}
<option>{{ piece|e }}</option>
{% endfor %}
</select>
<input name="is_tome" id="is_tome" title="is tome" type="checkbox"/>
<input name="action" id="action" type="hidden" value="add"/>
<input name="method" id="method" type="hidden" value="post"/>
<button>add</button>
</form>
<form action="/bis" method="post">
<select name="player" id="player" title="player">
{% for player in players %}
<option>{{ player.player|e }}</option>
{% endfor %}
</select>
<input name="bis" id="bis" placeholder="player bis link" title="bis" type="text"/>
<input name="method" id="method" type="hidden" value="put"/>
<button>add</button>
</form>
<table id="result">
<tr>
<th>player</th>
<th>piece</th>
<th>is_tome</th>
<th></th>
</tr>
{% for player in players %}
<tr>
<td class="include_search">{{ player.player|e }}</td>
<td class="include_search">{{ player.piece|e }}</td>
<td>{{ player.is_tome|e }}</td>
<td>
<form action="/bis" method="post">
<input name="action" id="action" type="hidden" value="remove"/>
<button>remove</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% include "export_to_csv.jinja2" %}
{% include "root.jinja2" %}
{% include "search.jinja2" %}
</body>
</html>

3
templates/error.jinja2 Normal file
View File

@ -0,0 +1,3 @@
{% if request_error is defined and request_error is not none %}
<p id="error">Error occurs: {{ request_error|e }}</p>
{% endif %}

View File

@ -0,0 +1,35 @@
<button onclick="exportTableToCsv('result.csv')">Export to csv</button>
<script type="application/javascript">
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);
}
</script>

13
templates/index.jinja2 Normal file
View File

@ -0,0 +1,13 @@
<html lang="en">
<head>
<title>FFXIV loot helper</title>
</head>
<body>
<center>
<a href="/party" title="party"><h2>party</h2></a>
<a href="/bis" title="bis management"><h2>bis</h2></a>
<a href="/loot" title="loot management"><h2>loot</h2></a>
</center>
</body>
</html>

53
templates/party.jinja2 Normal file
View File

@ -0,0 +1,53 @@
<html lang="en">
<head>
<title>Party</title>
{% include "style.jinja2" %}
</head>
<body>
<h2>party</h2>
{% include "error.jinja2" %}
{% include "search_line.jinja2" %}
<form action="/party" method="post">
<input name="nick" id="nick" placeholder="player nick name" title="nick" type="text"/>
<input name="job" id="job" placeholder="player job" title="job" type="text"/>
<input name="bis" id="bis" placeholder="player bis link" title="bis" type="text"/>
<input name="priority" id="priority" placeholder="player priority" title="priority" type="number" value="0"/>
<input name="action" id="action" type="hidden" value="add"/>
<button>add</button>
</form>
<table id="result">
<tr>
<th>nick</th>
<th>job</th>
<th>bis pieces looted</th>
<th>total pieces looted</th>
<th>priority</th>
<th></th>
</tr>
{% for player in players %}
<tr>
<td class="include_search">{{ player.nick|e }}</td>
<td class="include_search">{{ player.job|e }}</td>
<td>{{ player.loot_count_bis|e }}</td>
<td>{{ player.loot_count|e }}</td>
<td>{{ player.priority|e }}</td>
<td>
<form action="/party" method="post">
<input name="action" id="action" type="hidden" value="remove"/>
<button>remove</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% include "export_to_csv.jinja2" %}
{% include "root.jinja2" %}
{% include "search.jinja2" %}
</body>
</html>

1
templates/root.jinja2 Normal file
View File

@ -0,0 +1 @@
<p><a href="/" title="root">root</a></p>

23
templates/search.jinja2 Normal file
View File

@ -0,0 +1,23 @@
<script type="text/javascript">
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;
}
}
</script>

View File

@ -0,0 +1,3 @@
<div>
<input type="text" id="search" onkeyup="searchTable()" placeholder="search for data" title="search"/>
</div>

6
templates/style.jinja2 Normal file
View File

@ -0,0 +1,6 @@
<style>
table { border-collapse: collapse; }
table, th, td { border: 1px solid black; padding: 5px; }
input { margin: 5px; }
#error { color: #ff0000; }
</style>