@@ -141,12 +141,23 @@ class CampaignDB: | |||
res = self._conn.execute(query, (campaign, operation)).fetchall() | |||
return tuple(res[0]) | |||
def get_associated_kills(self, campaign, operation, limit=5, offset=0): | |||
def get_associated_kills(self, campaign, operation, sort="new", limit=-1, | |||
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. | |||
Kills are returned as dictionaries, up to a limit. Use -1 for no limit. | |||
The sort should be "new" for most recent first, "old" for most recent | |||
last, or "value" for most valuable first. | |||
""" | |||
sortkeys = { | |||
"new": "ok_killid DESC", | |||
"old": "ok_killid ASC", | |||
"value": "kill_value DESC, ok_killid DESC" | |||
} | |||
if sort in sortkeys: | |||
sortkey = sortkeys[sort] | |||
else: | |||
raise ValueError(sort) | |||
if not isinstance(limit, int): | |||
raise ValueError(limit) | |||
if not isinstance(offset, int): | |||
@@ -160,8 +171,8 @@ class CampaignDB: | |||
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) | |||
ORDER BY {} LIMIT {} OFFSET {}""" | |||
qform = query.format(sortkey, limit, offset) | |||
res = self._conn.execute(qform, (campaign, operation)).fetchall() | |||
return [{ | |||
@@ -205,21 +216,34 @@ class CampaignDB: | |||
for char_id, types in chars.items() | |||
for type_id, (count, value) in types.items()]) | |||
def get_associated_items(self, campaign, operation, limit=5, offset=0): | |||
def get_associated_items(self, campaign, operation, sort="value", limit=-1, | |||
offset=0): | |||
"""Return a list of items associated with a campaign/operation. | |||
Items are returned as 2-tuples of (item_type, item_count), most | |||
valuable first, up to a limit. Use -1 for no limit. | |||
Items are returned as 2-tuples of (item_type, item_count), up to a | |||
limit. Use -1 for no limit. The sort should be "value" for most | |||
valuable first (individual item value * quantity), "quantity" for | |||
greatest quantity first, "price" for most valuable items first. | |||
""" | |||
sortkeys = { | |||
"value": "total_value DESC", | |||
"quantity": "total_count DESC", | |||
"price": "(total_value / total_count) DESC" | |||
} | |||
if sort in sortkeys: | |||
sortkey = sortkeys[sort] | |||
else: | |||
raise ValueError(sort) | |||
if not isinstance(limit, int): | |||
raise ValueError(limit) | |||
if not isinstance(offset, int): | |||
raise ValueError(offset) | |||
query = """SELECT oi_type, SUM(oi_count), TOTAL(oi_value) as total_val | |||
query = """SELECT oi_type, SUM(oi_count) AS total_count, | |||
TOTAL(oi_value) as total_value | |||
FROM oper_item | |||
WHERE oi_campaign = ? AND oi_operation = ? | |||
GROUP BY oi_type ORDER BY total_val DESC LIMIT {} OFFSET {}""" | |||
qform = query.format(limit, offset) | |||
GROUP BY oi_type ORDER BY {} LIMIT {} OFFSET {}""" | |||
qform = query.format(sortkey, limit, offset) | |||
res = self._conn.execute(qform, (campaign, operation)).fetchall() | |||
return [(type_id, count or 0, value) for type_id, count, value in res] |
@@ -55,18 +55,25 @@ def get_overview(cname, opname): | |||
"operation=%s", age, cname, opname) | |||
return g.campaign_db.get_overview(cname, opname) | |||
def get_summary(cname, opname, limit=5): | |||
"""Return a sample fraction of results for the given campaign/operation.""" | |||
def get_summary(cname, opname, sortby=None, limit=5): | |||
"""Return some details for the given campaign/operation.""" | |||
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" | |||
sorts = ["new", "old", "value"] | |||
renderer = "killboard_recent" | |||
func = g.campaign_db.get_associated_kills | |||
elif optype == "collection": | |||
items = g.campaign_db.get_associated_items(cname, opname, limit=limit) | |||
return items, "collection_items" | |||
sorts = ["value", "quantity", "price"] | |||
renderer = "collection_items" | |||
func = g.campaign_db.get_associated_items | |||
else: | |||
raise RuntimeError("Unknown operation type: %s" % optype) | |||
if sortby not in sorts: | |||
sortby = sorts[0] | |||
data = func(cname, opname, sort=sortby, limit=limit) | |||
return data, renderer | |||
def get_unit(operation, num, primary=True): | |||
"""Return the correct form of the unit tracked by the given operation.""" | |||
if not primary: | |||
@@ -52,10 +52,11 @@ def operation(cname, opname): | |||
if opname not in campaign["operations"]: | |||
abort(404) | |||
operation = campaign["operations"][opname] | |||
sortby = request.args.get("sort") | |||
enabled = cname in config["enabled"] and opname in campaign["enabled"] | |||
return render_template("campaigns/operation.mako", | |||
cname=cname, campaign=campaign, opname=opname, | |||
operation=operation, enabled=enabled) | |||
operation=operation, sortby=sortby, enabled=enabled) | |||
@blueprint.rroute("/settings/campaign", methods=["POST"]) | |||
def set_campaign(): | |||
@@ -218,7 +218,36 @@ h2 .disabled-info { | |||
.operation.detail .detail-list li:not(:last-child)::after { | |||
margin-left: 0.25em; | |||
content: "/"; | |||
color: #AAA; | |||
color: #777; | |||
} | |||
.change-sort { | |||
display: inline-block; | |||
margin: 0; | |||
padding: 0; | |||
} | |||
.change-sort li { | |||
display: inline-block; | |||
} | |||
.change-sort .cur { | |||
font-weight: bold; | |||
} | |||
.change-sort::before { | |||
content: "["; | |||
color: #777; | |||
} | |||
.change-sort::after { | |||
content: "]"; | |||
color: #777; | |||
} | |||
.change-sort li:not(:last-child)::after { | |||
content: " |"; | |||
color: #777; | |||
} | |||
.last-updated { | |||
@@ -27,7 +27,7 @@ | |||
<% | |||
mod = g.config.modules.campaigns | |||
primary, secondary = mod.get_overview(cname, opname) | |||
summary, renderer = mod.get_summary(cname, opname, limit=-1) | |||
summary, renderer = mod.get_summary(cname, opname, sortby=sortby, limit=-1) | |||
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) | |||
@@ -48,7 +48,7 @@ | |||
</div> | |||
% if summary: | |||
<div class="summary"> | |||
${render_summary(renderer, summary, detail=True)} | |||
${render_summary(renderer, summary, detail=True, sortby=sortby)} | |||
</div> | |||
% endif | |||
</div> | |||
@@ -95,9 +95,32 @@ | |||
</td> | |||
</tr> | |||
</%def> | |||
<%def name="_killboard_recent(summary, detail)"> | |||
<%def name="_build_sort_changer(keys, descriptors, sortby)"> | |||
% if keys: | |||
<ul class="change-sort"> | |||
% for key in keys: | |||
% if key == sortby: | |||
<li class="cur">${descriptors[key]}</li> | |||
% else: | |||
<li><a href="${url_for('.operation', cname=cname, opname=opname, sort=key)}">${descriptors[key]}</a></li> | |||
% endif | |||
% endfor | |||
</ul> | |||
% endif | |||
</%def> | |||
<%def name="_killboard_recent(summary, detail, sortby)"> | |||
% if detail: | |||
<% | |||
descriptors = { | |||
"new": "most recent first", | |||
"old": "most recent last", | |||
"value": "most valuable first" | |||
} | |||
if sortby not in descriptors: | |||
sortby = "new" | |||
%> | |||
<h3 class="head">Kills:</h3> | |||
${_build_sort_changer(["new", "old", "value"], descriptors, sortby)} | |||
% else: | |||
<div class="head">Most recent kills:</div> | |||
% endif | |||
@@ -113,9 +136,19 @@ | |||
</table> | |||
</div> | |||
</%def> | |||
<%def name="_collection_items(summary, detail)"> | |||
<%def name="_collection_items(summary, detail, sortby)"> | |||
% if detail: | |||
<% | |||
descriptors = { | |||
"value": "most valuable first", | |||
"quantity": "greatest quantity first", | |||
"price": "most expensive first" | |||
} | |||
if sortby not in descriptors: | |||
sortby = "value" | |||
%> | |||
<h3 class="head">Items:</h3> | |||
${_build_sort_changer(["value", "quantity", "price"], descriptors, sortby)} | |||
% else: | |||
<div class="head">Top items:</div> | |||
% endif | |||
@@ -128,10 +161,10 @@ | |||
</div> | |||
</%def> | |||
<%def name="render_summary(renderer, summary, detail=False)"><% | |||
<%def name="render_summary(renderer, summary, detail=False, sortby=None)"><% | |||
if renderer == "killboard_recent": | |||
return _killboard_recent(summary, detail) | |||
return _killboard_recent(summary, detail, sortby) | |||
if renderer == "collection_items": | |||
return _collection_items(summary, detail) | |||
return _collection_items(summary, detail, sortby) | |||
raise RuntimeError("Unknown renderer: %s" % renderer) | |||
%></%def> |