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