@@ -2,8 +2,16 @@ from datetime import datetime, timedelta | |||
import humanize | |||
__all__ = ["format_isk", "format_isk_compact", "format_utctime", | |||
"format_utctime_compact", "format_security", "get_security_class"] | |||
__all__ = [ | |||
"format_quantity", "format_isk", "format_isk_compact", "format_utctime", | |||
"format_utctime_compact", "format_security", "get_security_class" | |||
] | |||
def format_quantity(value): | |||
"""Nicely format an integer quantity.""" | |||
if value < 10**6: | |||
return "{:,}".format(value) | |||
return humanize.intword(value, "%.2f") | |||
def format_isk(value): | |||
"""Nicely format an ISK value.""" | |||
@@ -187,7 +187,7 @@ class CampaignDB: | |||
The data should be a multi-layered dictionary. It maps operation names | |||
to a dict that maps character IDs to a dict that maps type IDs to | |||
integer counts. | |||
tuples of integer counts and float values. | |||
""" | |||
with self._conn as conn: | |||
cur = conn.execute("BEGIN TRANSACTION") | |||
@@ -195,29 +195,31 @@ class CampaignDB: | |||
cur.execute(query, (campaign,)) | |||
query = """INSERT INTO oper_item ( | |||
oi_campaign, oi_operation, oi_character, oi_type, oi_count) | |||
VALUES (?, ?, ?, ?, ?)""" | |||
oi_campaign, oi_operation, oi_character, oi_type, oi_count, | |||
oi_value) | |||
VALUES (?, ?, ?, ?, ?, ?)""" | |||
cur.executemany(query, [ | |||
(campaign, operation, int(char_id), int(type_id), int(count)) | |||
(campaign, operation, int(char_id), int(type_id), int(count), | |||
float(value)) | |||
for operation, chars in data.items() | |||
for char_id, types in chars.items() | |||
for type_id, count in types.items()]) | |||
for type_id, (count, value) in types.items()]) | |||
def get_associated_items(self, campaign, operation, limit=5, offset=0): | |||
"""Return a list of items associated with a campaign/operation. | |||
Items are returned as 2-tuples of (item_type, item_count), most recent | |||
first, up to a limit. Use -1 for no limit. | |||
Items are returned as 2-tuples of (item_type, item_count), most | |||
valuable first, up to a limit. Use -1 for no limit. | |||
""" | |||
if not isinstance(limit, int): | |||
raise ValueError(limit) | |||
if not isinstance(offset, int): | |||
raise ValueError(offset) | |||
query = """SELECT oi_type, SUM(oi_count) as total_count | |||
query = """SELECT oi_type, SUM(oi_count), TOTAL(oi_value) as total_val | |||
FROM oper_item | |||
WHERE oi_campaign = ? AND oi_operation = ? | |||
GROUP BY oi_type ORDER BY total_count DESC LIMIT {} OFFSET {}""" | |||
GROUP BY oi_type ORDER BY total_val DESC LIMIT {} OFFSET {}""" | |||
qform = query.format(limit, offset) | |||
res = self._conn.execute(qform, (campaign, operation)).fetchall() | |||
return [(type_id, count or 0) for type_id, count in res] | |||
return [(type_id, count or 0, value) for type_id, count, value in res] |
@@ -81,23 +81,22 @@ def _update_killboard_operations(cname, opnames, min_kill_id): | |||
secondary = None | |||
_save_operation(cname, opname, primary, secondary, key=max_kill_id) | |||
def _get_prices(): | |||
"""Return a dict mapping type IDs to ISK prices.""" | |||
pricelist = g.eve.esi().v1.markets.prices.get() | |||
return {entry["type_id"]: entry["average_price"] | |||
for entry in pricelist if "average_price" in entry} | |||
def _save_collection_overview(cname, opnames, data): | |||
"""Save collection overview data to the database.""" | |||
operations = config["campaigns"][cname]["operations"] | |||
if any(operations[opname].get("isk", True) for opname in opnames): | |||
pricelist = g.eve.esi().v1.markets.prices.get() | |||
prices = {entry["type_id"]: entry["average_price"] | |||
for entry in pricelist if "average_price" in entry} | |||
else: | |||
prices = {} | |||
for opname in opnames: | |||
primary = sum(sum(d.values()) for d in data[opname].values()) | |||
primary = sum(count for d in data[opname].values() | |||
for (count, _) in d.values()) | |||
show_isk = operations[opname].get("isk", True) | |||
if show_isk: | |||
secondary = sum(prices.get(typeid, 0.0) * count | |||
for d in data[opname].values() | |||
for typeid, count in d.items()) | |||
secondary = sum(value for d in data[opname].values() | |||
for (_, value) in d.values()) | |||
else: | |||
secondary = None | |||
_save_operation(cname, opname, primary, secondary) | |||
@@ -110,6 +109,7 @@ def _update_collection_operations(cname, opnames): | |||
qualif = operations[opname]["qualifiers"] | |||
filters.append((_build_filter(qualif, "asset"), opname)) | |||
prices = _get_prices() | |||
data = {opname: {} for opname in opnames} | |||
for char_id, token in g.auth.get_valid_characters(): | |||
@@ -131,11 +131,13 @@ def _update_collection_operations(cname, opnames): | |||
if filt(asset): | |||
typeid = asset["type_id"] | |||
count = 1 if asset["is_singleton"] else asset["quantity"] | |||
value = prices.get(typeid, 0.0) | |||
char = data[opname][char_id] | |||
if typeid in char: | |||
char[typeid] += count | |||
char[typeid][0] += count | |||
char[typeid][1] += count * value | |||
else: | |||
char[typeid] = count | |||
char[typeid] = [count, count * value] | |||
g.campaign_db.update_items(cname, data) | |||
_save_collection_overview(cname, opnames, data) | |||
@@ -47,6 +47,7 @@ campaigns: | |||
isk: false | |||
# Report as "10 units" / "1 unit" of Tritanium | |||
unit: unit|units | |||
# Python function to filter items: | |||
qualifiers: |- | |||
type = g.eve.universe.type(asset["type_id"]) | |||
return type.name == "Tritanium" | |||
@@ -60,6 +60,7 @@ CREATE TABLE oper_item ( | |||
oi_character INTEGER, | |||
oi_type INTEGER, | |||
oi_count INTEGER, | |||
oi_value REAL, | |||
UNIQUE (oi_campaign, oi_operation, oi_character, oi_type) | |||
); | |||
@@ -519,13 +519,16 @@ h2 .disabled-info { | |||
border-bottom: none; | |||
} | |||
.operation .itemboard .num { | |||
.operation .itemboard td:last-child { | |||
padding-left: 0.5em; | |||
text-align: right; | |||
} | |||
.operation .itemboard .count { | |||
font-weight: bold; | |||
} | |||
.operation .itemboard .num::before { | |||
.operation .itemboard .count::before { | |||
content: "×"; | |||
font-weight: normal; | |||
color: #AAA; | |||
@@ -1,7 +1,7 @@ | |||
<%! | |||
from calefaction.format import ( | |||
format_isk_compact, format_utctime_compact, format_security, | |||
get_security_class) | |||
format_quantity, format_isk_compact, format_utctime_compact, | |||
format_security, get_security_class) | |||
%> | |||
<%def name="_killboard_kill(kill)"> | |||
<% | |||
@@ -56,13 +56,20 @@ | |||
</%def> | |||
<%def name="_itemboard_item(item)"> | |||
<% | |||
type_id, count = item | |||
type_id, count, value = item | |||
type = g.eve.universe.type(type_id) | |||
%> | |||
<tr> | |||
<td class="icon"><img title="${type.name | h}" alt="" src="${g.eve.image.inventory(type_id, 64)}"/></td> | |||
<td><a href="https://eve-central.com/home/quicklook.html?typeid=${type_id | u}">${type.name | h}</a></td> | |||
<td class="num">${count | h}</td> | |||
<td class="icon"> | |||
<img title="${type.name | h}" alt="" src="${g.eve.image.inventory(type_id, 64)}"/> | |||
</td> | |||
<td> | |||
<a href="https://eve-central.com/home/quicklook.html?typeid=${type_id | u}">${type.name | h}</a> | |||
</td> | |||
<td> | |||
<span class="count">${format_quantity(count) | h}</span><br/> | |||
<abbr class="price" title="${"{:,.2f}".format(value)} ISK">${format_isk_compact(value) | h}</abbr> | |||
</td> | |||
</tr> | |||
</%def> | |||
<%def name="_killboard_recent(summary)"> | |||