@@ -2,8 +2,16 @@ from datetime import datetime, timedelta | |||||
import humanize | 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): | def format_isk(value): | ||||
"""Nicely format an 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 | 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 | 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: | with self._conn as conn: | ||||
cur = conn.execute("BEGIN TRANSACTION") | cur = conn.execute("BEGIN TRANSACTION") | ||||
@@ -195,29 +195,31 @@ class CampaignDB: | |||||
cur.execute(query, (campaign,)) | cur.execute(query, (campaign,)) | ||||
query = """INSERT INTO oper_item ( | 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, [ | 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 operation, chars in data.items() | ||||
for char_id, types in chars.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): | def get_associated_items(self, campaign, operation, limit=5, offset=0): | ||||
"""Return a list of items associated with a campaign/operation. | """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): | if not isinstance(limit, int): | ||||
raise ValueError(limit) | raise ValueError(limit) | ||||
if not isinstance(offset, int): | if not isinstance(offset, int): | ||||
raise ValueError(offset) | 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 | FROM oper_item | ||||
WHERE oi_campaign = ? AND oi_operation = ? | 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) | qform = query.format(limit, offset) | ||||
res = self._conn.execute(qform, (campaign, operation)).fetchall() | 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 | secondary = None | ||||
_save_operation(cname, opname, primary, secondary, key=max_kill_id) | _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): | def _save_collection_overview(cname, opnames, data): | ||||
"""Save collection overview data to the database.""" | """Save collection overview data to the database.""" | ||||
operations = config["campaigns"][cname]["operations"] | 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: | 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) | show_isk = operations[opname].get("isk", True) | ||||
if show_isk: | 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: | else: | ||||
secondary = None | secondary = None | ||||
_save_operation(cname, opname, primary, secondary) | _save_operation(cname, opname, primary, secondary) | ||||
@@ -110,6 +109,7 @@ def _update_collection_operations(cname, opnames): | |||||
qualif = operations[opname]["qualifiers"] | qualif = operations[opname]["qualifiers"] | ||||
filters.append((_build_filter(qualif, "asset"), opname)) | filters.append((_build_filter(qualif, "asset"), opname)) | ||||
prices = _get_prices() | |||||
data = {opname: {} for opname in opnames} | data = {opname: {} for opname in opnames} | ||||
for char_id, token in g.auth.get_valid_characters(): | for char_id, token in g.auth.get_valid_characters(): | ||||
@@ -131,11 +131,13 @@ def _update_collection_operations(cname, opnames): | |||||
if filt(asset): | if filt(asset): | ||||
typeid = asset["type_id"] | typeid = asset["type_id"] | ||||
count = 1 if asset["is_singleton"] else asset["quantity"] | count = 1 if asset["is_singleton"] else asset["quantity"] | ||||
value = prices.get(typeid, 0.0) | |||||
char = data[opname][char_id] | char = data[opname][char_id] | ||||
if typeid in char: | if typeid in char: | ||||
char[typeid] += count | |||||
char[typeid][0] += count | |||||
char[typeid][1] += count * value | |||||
else: | else: | ||||
char[typeid] = count | |||||
char[typeid] = [count, count * value] | |||||
g.campaign_db.update_items(cname, data) | g.campaign_db.update_items(cname, data) | ||||
_save_collection_overview(cname, opnames, data) | _save_collection_overview(cname, opnames, data) | ||||
@@ -47,6 +47,7 @@ campaigns: | |||||
isk: false | isk: false | ||||
# Report as "10 units" / "1 unit" of Tritanium | # Report as "10 units" / "1 unit" of Tritanium | ||||
unit: unit|units | unit: unit|units | ||||
# Python function to filter items: | |||||
qualifiers: |- | qualifiers: |- | ||||
type = g.eve.universe.type(asset["type_id"]) | type = g.eve.universe.type(asset["type_id"]) | ||||
return type.name == "Tritanium" | return type.name == "Tritanium" | ||||
@@ -60,6 +60,7 @@ CREATE TABLE oper_item ( | |||||
oi_character INTEGER, | oi_character INTEGER, | ||||
oi_type INTEGER, | oi_type INTEGER, | ||||
oi_count INTEGER, | oi_count INTEGER, | ||||
oi_value REAL, | |||||
UNIQUE (oi_campaign, oi_operation, oi_character, oi_type) | UNIQUE (oi_campaign, oi_operation, oi_character, oi_type) | ||||
); | ); | ||||
@@ -519,13 +519,16 @@ h2 .disabled-info { | |||||
border-bottom: none; | border-bottom: none; | ||||
} | } | ||||
.operation .itemboard .num { | |||||
.operation .itemboard td:last-child { | |||||
padding-left: 0.5em; | padding-left: 0.5em; | ||||
text-align: right; | text-align: right; | ||||
} | |||||
.operation .itemboard .count { | |||||
font-weight: bold; | font-weight: bold; | ||||
} | } | ||||
.operation .itemboard .num::before { | |||||
.operation .itemboard .count::before { | |||||
content: "×"; | content: "×"; | ||||
font-weight: normal; | font-weight: normal; | ||||
color: #AAA; | color: #AAA; | ||||
@@ -1,7 +1,7 @@ | |||||
<%! | <%! | ||||
from calefaction.format import ( | 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)"> | <%def name="_killboard_kill(kill)"> | ||||
<% | <% | ||||
@@ -56,13 +56,20 @@ | |||||
</%def> | </%def> | ||||
<%def name="_itemboard_item(item)"> | <%def name="_itemboard_item(item)"> | ||||
<% | <% | ||||
type_id, count = item | |||||
type_id, count, value = item | |||||
type = g.eve.universe.type(type_id) | type = g.eve.universe.type(type_id) | ||||
%> | %> | ||||
<tr> | <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> | </tr> | ||||
</%def> | </%def> | ||||
<%def name="_killboard_recent(summary)"> | <%def name="_killboard_recent(summary)"> | ||||