| @@ -0,0 +1,66 @@ | |||
| from datetime import datetime, timedelta | |||
| import humanize | |||
| __all__ = ["format_isk", "format_isk_compact", "format_utctime", | |||
| "format_utctime_compact"] | |||
| def format_isk(value): | |||
| """Nicely format an ISK value.""" | |||
| if value < 10**6: | |||
| return "{:,.2f}".format(value) | |||
| return humanize.intword(value, "%.2f") | |||
| def format_isk_compact(value): | |||
| """Nicely format an ISK value compactly.""" | |||
| # Based on humanize.intword(). | |||
| powers = [10 ** x for x in [3, 6, 9, 12, 15]] | |||
| letters = ["k", "m", "b", "t", "q"] | |||
| if value < powers[0]: | |||
| return "{:,.2f}".format(value) | |||
| for ordinal, power in enumerate(powers[1:], 1): | |||
| if value < power: | |||
| chopped = value / float(powers[ordinal - 1]) | |||
| return "{:,.2f}{}".format(chopped, letters[ordinal - 1]) | |||
| return str(value) | |||
| def format_utctime(value): | |||
| """Format a UTC timestamp.""" | |||
| return humanize.naturaltime(datetime.utcnow() - value) | |||
| def _format_compact_delta(delta): | |||
| """Return a snippet of formatting for a time delta.""" | |||
| # Based on humanize.naturaldelta(). | |||
| seconds = abs(delta.seconds) | |||
| days = abs(delta.days) | |||
| years = days // 365 | |||
| days = days % 365 | |||
| months = int(days // 30.5) | |||
| if years == 0 and days < 1: | |||
| if seconds < 60: | |||
| return "{}s".format(seconds) | |||
| if seconds < 3600: | |||
| minutes = seconds // 60 | |||
| return "{}m".format(minutes) | |||
| hours = seconds // 3600 | |||
| return "{}h".format(hours) | |||
| if years == 0: | |||
| if months == 0: | |||
| return "{}d".format(days) | |||
| return "{}mo".format(months) | |||
| if years == 1: | |||
| if months == 0: | |||
| if days == 0: | |||
| return "1y" | |||
| return "1y {}d".format(days) | |||
| return "1y {}mo".format(months) | |||
| return "{}y".format(years) | |||
| def format_utctime_compact(value): | |||
| """Format a UTC timestamp compactly.""" | |||
| delta = datetime.utcnow() - value | |||
| if delta < timedelta(seconds=1): | |||
| return "just now" | |||
| return "{} ago".format(_format_compact_delta(delta)) | |||
| @@ -87,6 +87,8 @@ def _update_collection_operations(cname, opnames): | |||
| operation = campaign["operations"][opname] | |||
| show_isk = operation.get("isk", True) | |||
| # store per-user counts; update for all users in corp who have fresh | |||
| # API keys and leave other data stale | |||
| ... | |||
| primary = __import__("random").randint(10, 99) | |||
| secondary = __import__("random").randint(10000000, 5000000000) / 100 \ | |||
| @@ -31,6 +31,11 @@ h2, h3 { | |||
| margin: 0.5em 0; | |||
| } | |||
| abbr[title], acronym[title] { | |||
| border-bottom: 1px dotted #555; | |||
| text-decoration: none; | |||
| } | |||
| .understate { | |||
| font-weight: normal; | |||
| } | |||
| @@ -393,13 +398,88 @@ h2 .disabled-info { | |||
| font-size: 150%; | |||
| } | |||
| .operation .secondary { | |||
| font-size: 105%; | |||
| } | |||
| .operation .unit { | |||
| font-style: italic; | |||
| } | |||
| .operation .summary .head { | |||
| margin-top: 1em; | |||
| font-size: 14px; | |||
| } | |||
| .operation .summary .contents { | |||
| position: relative; | |||
| margin-top: 0.5em; | |||
| border: 1px solid #282828; | |||
| font-size: 14px; | |||
| } | |||
| .operation .killboard { | |||
| border-spacing: 0; | |||
| border-collapse: collapse; | |||
| text-align: left; | |||
| } | |||
| .operation .killboard:not(.expanded) tr:nth-child(2n) { | |||
| background-color: #181818; | |||
| } | |||
| .operation .killboard:not(.expanded) tr:nth-child(2n+1) { | |||
| background-color: #0A0A0A; | |||
| } | |||
| .operation .killboard td { | |||
| padding: 0.25em 0; | |||
| } | |||
| .operation .killboard td:first-child { | |||
| padding-left: 1em; | |||
| } | |||
| .operation .killboard td:last-child { | |||
| padding-right: 1em; | |||
| } | |||
| .operation .killboard .fluid { | |||
| padding-right: 0.5em; | |||
| } | |||
| .operation .killboard .icon { | |||
| width: 46px; | |||
| } | |||
| .operation .killboard.expanded { | |||
| position: absolute; | |||
| z-index: 1; | |||
| transition: clip-path 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); | |||
| clip-path: inset(0 100% 0 0); | |||
| } | |||
| .operation .killboard:not(.expanded) .extra { | |||
| display: none; | |||
| } | |||
| .operation .killboard.expanded .spacer { | |||
| display: none; | |||
| } | |||
| .operation .killboard img { | |||
| width: 42px; | |||
| height: 42px; | |||
| vertical-align: middle; | |||
| } | |||
| .operation .killboard abbr { | |||
| border-bottom: none; | |||
| } | |||
| @media (min-width: 800px) { | |||
| #operations { | |||
| margin: 1em 2em; | |||
| margin: 1em 0; | |||
| } | |||
| #operations section { | |||
| @@ -429,6 +509,10 @@ h2 .disabled-info { | |||
| margin: 0.5em 0; | |||
| } | |||
| .operation .overview { | |||
| margin-left: 1em; | |||
| } | |||
| .operation .primary, .operation .primary .unit { | |||
| display: inline-block; | |||
| } | |||
| @@ -436,6 +520,10 @@ h2 .disabled-info { | |||
| .operation .unit { | |||
| margin-left: 0.15em; | |||
| } | |||
| .operation .killboard:not(.expanded) { | |||
| width: 100%; | |||
| } | |||
| } | |||
| /* -------------------------------- Members -------------------------------- */ | |||
| @@ -64,4 +64,23 @@ $(function() { | |||
| this.form.submit(); | |||
| }); | |||
| $('#campaigns-select input[type="submit"]').hide(); | |||
| //Campaigns: selectively reveal operation summary details: | |||
| $(".operation .killboard tr").mouseenter(function() { | |||
| var div = $("<table>", {addClass: "killboard expanded"}) | |||
| .css($(this).position()) | |||
| .css("background-color", $(this).css("background-color")) | |||
| .css("position", "fixed") | |||
| .append($("<tr>").html($(this).html())) | |||
| .mouseleave(function() { $(this).remove(); }); | |||
| div.find(".spacer").remove(); | |||
| $(this).closest(".summary").find(".expanded").remove(); | |||
| $(this).closest(".contents").prepend(div); | |||
| div.css("width", Math.max(div.width(), $(this).width())); | |||
| div.css("position", ""); | |||
| div.css("clip-path", "inset(0 0% 0 0)"); | |||
| }); | |||
| $(".operation .summary").mouseleave(function() { | |||
| $(this).find(".expanded").remove(); | |||
| }); | |||
| }); | |||
| @@ -1,4 +1,6 @@ | |||
| <%! import humanize %> | |||
| <%! | |||
| from calefaction.format import format_isk | |||
| %> | |||
| <%inherit file="../_default.mako"/> | |||
| <%namespace file="renderers.mako" import="render_summary"/> | |||
| <%block name="title"> | |||
| @@ -37,14 +39,16 @@ | |||
| % if secondary is not None: | |||
| <div class="secondary"> | |||
| <abbr title="${"{:,.2f}".format(secondary)} ${sunit}"> | |||
| <span class="num">${humanize.intword(secondary) | h}</span> | |||
| <span class="num">${format_isk(secondary) | h}</span> | |||
| <span class="unit">${sunit}</span> | |||
| </abbr> | |||
| </div> | |||
| % endif | |||
| </div> | |||
| % if summary: | |||
| ${render_summary(renderer, summary)} | |||
| <div class="summary"> | |||
| ${render_summary(renderer, summary)} | |||
| </div> | |||
| % endif | |||
| </div> | |||
| % endfor | |||
| @@ -1,25 +1,57 @@ | |||
| <%! import humanize %> | |||
| <%! | |||
| from calefaction.format import format_isk_compact, format_utctime_compact | |||
| %> | |||
| <%def name="_killboard_kill(kill)"> | |||
| <% victim = kill["victim"] %> | |||
| <tr> | |||
| <td class="fluid"> | |||
| <span> | |||
| <abbr title="${kill["date"].strftime("%Y-%m-%d %H:%M")}">${format_utctime_compact(kill["date"]) | h}</abbr><br/> | |||
| <abbr title="${"{:,.2f}".format(kill["value"])} ISK">${format_isk_compact(kill["value"]) | h}</abbr> | |||
| </span> | |||
| </td> | |||
| <td class="fluid extra"> | |||
| <span> | |||
| ${kill["system"]} 0.3<br/><!-- ... --> | |||
| Region<!-- ... --> | |||
| </span> | |||
| </td> | |||
| <td class="icon"> | |||
| <a href="https://zkillboard.com/kill/${kill['id']}/"><img title="" alt="<!-- ... -->" src="${g.eve.image.inventory(victim["ship_id"], 64)}"/></a> | |||
| </td> | |||
| <td class="icon extra"> | |||
| <a href="https://zkillboard.com/character/${victim['char_id']}/"><img title="" alt="<!-- ... -->" src="${g.eve.image.character(victim["char_id"], 128)}"/></a> | |||
| </td> | |||
| <td class="icon${' extra' if victim["alliance_id"] and victim["faction_id"] else ''}"> | |||
| <a href="https://zkillboard.com/corporation/${victim['corp_id']}/"><img title="" alt="<!-- ... -->" src="${g.eve.image.corp(victim["corp_id"], 128)}"/></a> | |||
| </td> | |||
| <td class="icon${'' if victim["alliance_id"] else ' extra'}"> | |||
| % if victim["alliance_id"]: | |||
| <a href="https://zkillboard.com/alliance/${victim['alliance_id']}/"><img title="" alt="<!-- ... -->" src="${g.eve.image.alliance(victim["alliance_id"], 128)}"/></a> | |||
| % endif | |||
| </td> | |||
| <td class="icon${'' if victim["faction_id"] else ' extra'}"> | |||
| % if victim["faction_id"]: | |||
| <a href="https://zkillboard.com/faction/${victim['faction_id']}/"><img title="" alt="<!-- ... -->" src="${g.eve.image.faction(victim["faction_id"], 128)}"/></a> | |||
| % endif | |||
| </td> | |||
| % if not victim["alliance_id"] and not victim["faction_id"]: | |||
| <td class="icon spacer"></td> | |||
| % endif | |||
| </tr> | |||
| </%def> | |||
| <%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> | |||
| <div class="head">Most recent kills:</div> | |||
| <div class="contents"> | |||
| <table class="killboard"> | |||
| % for kill in summary: | |||
| ${_killboard_kill(kill)} | |||
| % endfor | |||
| </table> | |||
| </div> | |||
| </%def> | |||
| <%def name="render_summary(renderer, summary)"><% | |||
| if renderer == "killboard_recent": | |||
| return _killboard_recent(summary) | |||
| else: | |||
| raise RuntimeError("Unknown renderer: %s" % renderer) | |||
| raise RuntimeError("Unknown renderer: %s" % renderer) | |||
| %></%def> | |||