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