| @@ -98,6 +98,7 @@ class CampaignDB: | |||||
| except ValueError: | except ValueError: | ||||
| raise RuntimeError("Invalid kill_date=%s for kill_id=%d" % ( | raise RuntimeError("Invalid kill_date=%s for kill_id=%d" % ( | ||||
| kill["killTime"], kill["killID"])) | kill["killTime"], kill["killID"])) | ||||
| # ... Ensure IDs are all ints | |||||
| query = """INSERT OR REPLACE INTO kill ( | query = """INSERT OR REPLACE INTO kill ( | ||||
| kill_id, kill_date, kill_system, kill_victim_shipid, | kill_id, kill_date, kill_system, kill_victim_shipid, | ||||
| @@ -135,3 +136,34 @@ class CampaignDB: | |||||
| WHERE ok_campaign = ? AND ok_operation = ?""" | WHERE ok_campaign = ? AND ok_operation = ?""" | ||||
| res = self._conn.execute(query, (campaign, operation)).fetchall() | res = self._conn.execute(query, (campaign, operation)).fetchall() | ||||
| return tuple(res[0]) | return tuple(res[0]) | ||||
| def get_associated_kills(self, campaign, operation, limit=5, offset=0): | |||||
| """Return a list of kills associated with a campaign/operation. | |||||
| Kills are returned as dictionaries most recent 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 kill_id, kill_date, kill_system, kill_victim_shipid, | |||||
| kill_victim_charid, kill_victim_corpid, kill_victim_allianceid, | |||||
| kill_victim_factionid, kill_value | |||||
| FROM oper_kill | |||||
| JOIN kill ON ok_killid = kill_id | |||||
| WHERE ok_campaign = ? AND ok_operation = ? | |||||
| ORDER BY ok_killid DESC LIMIT {} OFFSET {}""" | |||||
| qform = query.format(limit, offset) | |||||
| res = self._conn.execute(qform, (campaign, operation)).fetchall() | |||||
| return [{ | |||||
| "id": row[0], | |||||
| "date": datetime.strptime(row[1], "%Y-%m-%d %H:%M:%S"), | |||||
| "system": row[2], | |||||
| "victim": { | |||||
| "ship_id": row[3], "char_id": row[4], "corp_id": row[5], | |||||
| "alliance_id": row[6], "faction_id": row[7]}, | |||||
| "value": row[8] | |||||
| } for row in res] | |||||
| @@ -53,12 +53,19 @@ def get_overview(cname, opname): | |||||
| else: | else: | ||||
| logger.debug("Using cache (age=%d) for campaign=%s " | logger.debug("Using cache (age=%d) for campaign=%s " | ||||
| "operation=%s", age, cname, opname) | "operation=%s", age, cname, opname) | ||||
| return g.campaign_db.get_overview(cname, opname) | |||||
| return g.campaign_db.get_overview(cname, opname) | |||||
| def get_summary(name, opname, limit=5): | |||||
| def get_summary(cname, opname, limit=5): | |||||
| """Return a sample fraction of results for the given campaign/operation.""" | """Return a sample fraction of results for the given campaign/operation.""" | ||||
| ... | |||||
| return [] | |||||
| optype = config["campaigns"][cname]["operations"][opname]["type"] | |||||
| if optype == "killboard": | |||||
| kills = g.campaign_db.get_associated_kills(cname, opname, limit=limit) | |||||
| return kills, "killboard_recent" | |||||
| elif optype == "collection": | |||||
| ... | |||||
| return [], None | |||||
| else: | |||||
| raise RuntimeError("Unknown operation type: %s" % optype) | |||||
| def get_unit(operation, num, primary=True): | def get_unit(operation, num, primary=True): | ||||
| """Return the correct form of the unit tracked by the given operation.""" | """Return the correct form of the unit tracked by the given operation.""" | ||||
| @@ -1,5 +1,6 @@ | |||||
| Flask==0.11.1 | Flask==0.11.1 | ||||
| Flask-Mako==0.4 | Flask-Mako==0.4 | ||||
| humanize==0.5.1 | |||||
| PyYAML==3.12 | PyYAML==3.12 | ||||
| requests==2.12.4 | requests==2.12.4 | ||||
| uWSGI==2.0.14 | uWSGI==2.0.14 | ||||
| @@ -353,11 +353,11 @@ h2 .disabled { | |||||
| text-decoration: line-through; | text-decoration: line-through; | ||||
| } | } | ||||
| h2 .disabled::after { | |||||
| display: inline-block; | |||||
| padding-left: 0.75em; | |||||
| font-size: 75%; | |||||
| content: "✘"; | |||||
| h2 .disabled-info { | |||||
| margin-left: 0.5em; | |||||
| color: #989898; | |||||
| font-size: 85%; | |||||
| font-variant: none; | |||||
| } | } | ||||
| #operations { | #operations { | ||||
| @@ -1,10 +1,15 @@ | |||||
| <%! import humanize %> | |||||
| <%inherit file="../_default.mako"/> | <%inherit file="../_default.mako"/> | ||||
| <%namespace file="renderers.mako" import="render_summary"/> | |||||
| <%block name="title"> | <%block name="title"> | ||||
| ${self.maketitle(campaign["title"], "Campaigns")} | ${self.maketitle(campaign["title"], "Campaigns")} | ||||
| </%block> | </%block> | ||||
| <h2> | <h2> | ||||
| <span class="understate">Campaign:</span> | <span class="understate">Campaign:</span> | ||||
| <span${"" if enabled else ' class="disabled"'}>${campaign["title"] | h}</span> | <span${"" if enabled else ' class="disabled"'}>${campaign["title"] | h}</span> | ||||
| % if not enabled: | |||||
| <abbr class="disabled-info" title="Campaign inactive">✘</abbr> | |||||
| % endif | |||||
| </h2> | </h2> | ||||
| <% mod = g.config.modules.campaigns %> | <% mod = g.config.modules.campaigns %> | ||||
| <div id="operations"> | <div id="operations"> | ||||
| @@ -15,8 +20,10 @@ | |||||
| <% | <% | ||||
| operation = campaign["operations"][opname] | operation = campaign["operations"][opname] | ||||
| primary, secondary = mod.get_overview(name, opname) | primary, secondary = mod.get_overview(name, opname) | ||||
| summary = mod.get_summary(name, opname, limit=5) | |||||
| summary, renderer = mod.get_summary(name, opname, limit=5) | |||||
| klass = "big" if primary < 1000 else "medium" if primary < 1000000 else "small" | klass = "big" if primary < 1000 else "medium" if primary < 1000000 else "small" | ||||
| punit = mod.get_unit(operation, primary) | |||||
| sunit = mod.get_unit(operation, secondary, primary=False) | |||||
| %> | %> | ||||
| <div class="operation"> | <div class="operation"> | ||||
| <h3> | <h3> | ||||
| @@ -25,21 +32,19 @@ | |||||
| <div class="overview"> | <div class="overview"> | ||||
| <div class="primary"> | <div class="primary"> | ||||
| <span class="num ${klass}">${"{:,}".format(primary)}</span> | <span class="num ${klass}">${"{:,}".format(primary)}</span> | ||||
| <div class="unit">${mod.get_unit(operation, primary)}</div> | |||||
| <div class="unit">${punit}</div> | |||||
| </div> | </div> | ||||
| % if secondary is not None: | % if secondary is not None: | ||||
| <div class="secondary"> | <div class="secondary"> | ||||
| <span class="num">${"{:,.2f}".format(secondary)}</span> | |||||
| <span class="unit">${mod.get_unit(operation, secondary, primary=False)}</span> | |||||
| <abbr title="${"{:,.2f}".format(secondary)} ${sunit}"> | |||||
| <span class="num">${humanize.intword(secondary) | h}</span> | |||||
| <span class="unit">${sunit}</span> | |||||
| </abbr> | |||||
| </div> | </div> | ||||
| % endif | % endif | ||||
| </div> | </div> | ||||
| % if summary: | % if summary: | ||||
| <ul class="summary"> | |||||
| % for item in summary: | |||||
| <li>${item}</li> | |||||
| % endfor | |||||
| </ul> | |||||
| ${render_summary(renderer, summary)} | |||||
| % endif | % endif | ||||
| </div> | </div> | ||||
| % endfor | % endfor | ||||
| @@ -0,0 +1,25 @@ | |||||
| <%! import humanize %> | |||||
| <%def name="_killboard_recent(summary)"> | |||||
| <ul class="summary"> | |||||
| % for kill in summary: | |||||
| <li> | |||||
| <a href="https://zkillboard.com/kill/${kill['id']}/">${kill["id"]}</a> | |||||
| ${kill["system"]} | |||||
| <abbr title="${kill["date"].strftime("%Y-%m-%d %H:%M")}">${humanize.naturaltime(kill["date"]) | h}</abbr> | |||||
| <img src="${g.eve.image.render(kill["victim"]["ship_id"], 128)}"/> | |||||
| <img src="${g.eve.image.character(kill["victim"]["char_id"], 128)}"/> | |||||
| <img src="${g.eve.image.corp(kill["victim"]["corp_id"], 128)}"/> | |||||
| <img src="${g.eve.image.alliance(kill["victim"]["alliance_id"], 128)}"/> | |||||
| <img src="${g.eve.image.faction(kill["victim"]["faction_id"], 128)}"/> | |||||
| <abbr title="${"{:,.2f}".format(kill["value"])} ISK">${humanize.intword(kill["value"]) | h} ISK</abbr> | |||||
| </li> | |||||
| % endfor | |||||
| </ul> | |||||
| </%def> | |||||
| <%def name="render_summary(renderer, summary)"><% | |||||
| if renderer == "killboard_recent": | |||||
| return _killboard_recent(summary) | |||||
| else: | |||||
| raise RuntimeError("Unknown renderer: %s" % renderer) | |||||
| %></%def> | |||||
| @@ -8,7 +8,7 @@ | |||||
| % for member in members: | % for member in members: | ||||
| <li> | <li> | ||||
| <a href="${g.eve.image.character(member.id, 1024)}"> | <a href="${g.eve.image.character(member.id, 1024)}"> | ||||
| <img class="styled-border" title="${member.name}" alt="${member.name}'s Portrait" src="${g.eve.image.character(member.id, 256)}"/> | |||||
| <img class="styled-border" title="${member.name}" alt="${member.name}'s Portrait" src="${g.eve.image.character(member.id, 128)}"/> | |||||
| </a> | </a> | ||||
| % if member.roles: | % if member.roles: | ||||
| <span>${member.name}<em>${member.roles}</em></span> | <span>${member.name}<em>${member.roles}</em></span> | ||||