| @@ -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)"> | ||||