@@ -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] | operation = campaign["operations"][opname] | ||||
show_isk = operation.get("isk", True) | 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) | primary = __import__("random").randint(10, 99) | ||||
secondary = __import__("random").randint(10000000, 5000000000) / 100 \ | secondary = __import__("random").randint(10000000, 5000000000) / 100 \ | ||||
@@ -31,6 +31,11 @@ h2, h3 { | |||||
margin: 0.5em 0; | margin: 0.5em 0; | ||||
} | } | ||||
abbr[title], acronym[title] { | |||||
border-bottom: 1px dotted #555; | |||||
text-decoration: none; | |||||
} | |||||
.understate { | .understate { | ||||
font-weight: normal; | font-weight: normal; | ||||
} | } | ||||
@@ -393,13 +398,88 @@ h2 .disabled-info { | |||||
font-size: 150%; | font-size: 150%; | ||||
} | } | ||||
.operation .secondary { | |||||
font-size: 105%; | |||||
} | |||||
.operation .unit { | .operation .unit { | ||||
font-style: italic; | 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) { | @media (min-width: 800px) { | ||||
#operations { | #operations { | ||||
margin: 1em 2em; | |||||
margin: 1em 0; | |||||
} | } | ||||
#operations section { | #operations section { | ||||
@@ -429,6 +509,10 @@ h2 .disabled-info { | |||||
margin: 0.5em 0; | margin: 0.5em 0; | ||||
} | } | ||||
.operation .overview { | |||||
margin-left: 1em; | |||||
} | |||||
.operation .primary, .operation .primary .unit { | .operation .primary, .operation .primary .unit { | ||||
display: inline-block; | display: inline-block; | ||||
} | } | ||||
@@ -436,6 +520,10 @@ h2 .disabled-info { | |||||
.operation .unit { | .operation .unit { | ||||
margin-left: 0.15em; | margin-left: 0.15em; | ||||
} | } | ||||
.operation .killboard:not(.expanded) { | |||||
width: 100%; | |||||
} | |||||
} | } | ||||
/* -------------------------------- Members -------------------------------- */ | /* -------------------------------- Members -------------------------------- */ | ||||
@@ -64,4 +64,23 @@ $(function() { | |||||
this.form.submit(); | this.form.submit(); | ||||
}); | }); | ||||
$('#campaigns-select input[type="submit"]').hide(); | $('#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"/> | <%inherit file="../_default.mako"/> | ||||
<%namespace file="renderers.mako" import="render_summary"/> | <%namespace file="renderers.mako" import="render_summary"/> | ||||
<%block name="title"> | <%block name="title"> | ||||
@@ -37,14 +39,16 @@ | |||||
% if secondary is not None: | % if secondary is not None: | ||||
<div class="secondary"> | <div class="secondary"> | ||||
<abbr title="${"{:,.2f}".format(secondary)} ${sunit}"> | <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> | <span class="unit">${sunit}</span> | ||||
</abbr> | </abbr> | ||||
</div> | </div> | ||||
% endif | % endif | ||||
</div> | </div> | ||||
% if summary: | % if summary: | ||||
${render_summary(renderer, summary)} | |||||
<div class="summary"> | |||||
${render_summary(renderer, summary)} | |||||
</div> | |||||
% endif | % endif | ||||
</div> | </div> | ||||
% endfor | % 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)"> | <%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> | ||||
<%def name="render_summary(renderer, summary)"><% | <%def name="render_summary(renderer, summary)"><% | ||||
if renderer == "killboard_recent": | if renderer == "killboard_recent": | ||||
return _killboard_recent(summary) | return _killboard_recent(summary) | ||||
else: | |||||
raise RuntimeError("Unknown renderer: %s" % renderer) | |||||
raise RuntimeError("Unknown renderer: %s" % renderer) | |||||
%></%def> | %></%def> |