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