| @@ -141,12 +141,23 @@ class CampaignDB: | |||||
| res = self._conn.execute(query, (campaign, operation)).fetchall() | res = self._conn.execute(query, (campaign, operation)).fetchall() | ||||
| return tuple(res[0]) | 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. | """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): | if not isinstance(limit, int): | ||||
| raise ValueError(limit) | raise ValueError(limit) | ||||
| if not isinstance(offset, int): | if not isinstance(offset, int): | ||||
| @@ -160,8 +171,8 @@ class CampaignDB: | |||||
| FROM oper_kill | FROM oper_kill | ||||
| JOIN kill ON ok_killid = kill_id | JOIN kill ON ok_killid = kill_id | ||||
| WHERE ok_campaign = ? AND ok_operation = ? | 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() | res = self._conn.execute(qform, (campaign, operation)).fetchall() | ||||
| return [{ | return [{ | ||||
| @@ -205,21 +216,34 @@ class CampaignDB: | |||||
| for char_id, types in chars.items() | for char_id, types in chars.items() | ||||
| for type_id, (count, value) in types.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. | """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): | if not isinstance(limit, int): | ||||
| raise ValueError(limit) | raise ValueError(limit) | ||||
| if not isinstance(offset, int): | if not isinstance(offset, int): | ||||
| raise ValueError(offset) | 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 | FROM oper_item | ||||
| WHERE oi_campaign = ? AND oi_operation = ? | 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() | res = self._conn.execute(qform, (campaign, operation)).fetchall() | ||||
| return [(type_id, count or 0, value) for type_id, count, value in res] | 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) | "operation=%s", age, cname, opname) | ||||
| return g.campaign_db.get_overview(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"] | optype = config["campaigns"][cname]["operations"][opname]["type"] | ||||
| if optype == "killboard": | 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": | 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: | else: | ||||
| raise RuntimeError("Unknown operation type: %s" % optype) | 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): | def get_unit(operation, num, primary=True): | ||||
| """Return the correct form of the unit tracked by the given operation.""" | """Return the correct form of the unit tracked by the given operation.""" | ||||
| if not primary: | if not primary: | ||||
| @@ -52,10 +52,11 @@ def operation(cname, opname): | |||||
| if opname not in campaign["operations"]: | if opname not in campaign["operations"]: | ||||
| abort(404) | abort(404) | ||||
| operation = campaign["operations"][opname] | operation = campaign["operations"][opname] | ||||
| sortby = request.args.get("sort") | |||||
| enabled = cname in config["enabled"] and opname in campaign["enabled"] | enabled = cname in config["enabled"] and opname in campaign["enabled"] | ||||
| return render_template("campaigns/operation.mako", | return render_template("campaigns/operation.mako", | ||||
| cname=cname, campaign=campaign, opname=opname, | cname=cname, campaign=campaign, opname=opname, | ||||
| operation=operation, enabled=enabled) | |||||
| operation=operation, sortby=sortby, enabled=enabled) | |||||
| @blueprint.rroute("/settings/campaign", methods=["POST"]) | @blueprint.rroute("/settings/campaign", methods=["POST"]) | ||||
| def set_campaign(): | def set_campaign(): | ||||
| @@ -218,7 +218,36 @@ h2 .disabled-info { | |||||
| .operation.detail .detail-list li:not(:last-child)::after { | .operation.detail .detail-list li:not(:last-child)::after { | ||||
| margin-left: 0.25em; | margin-left: 0.25em; | ||||
| content: "/"; | 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 { | .last-updated { | ||||
| @@ -27,7 +27,7 @@ | |||||
| <% | <% | ||||
| mod = g.config.modules.campaigns | mod = g.config.modules.campaigns | ||||
| primary, secondary = mod.get_overview(cname, opname) | 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" | klass = "big" if primary < 1000 else "medium" if primary < 1000000 else "small" | ||||
| punit = mod.get_unit(operation, primary) | punit = mod.get_unit(operation, primary) | ||||
| sunit = mod.get_unit(operation, secondary, primary=False) | sunit = mod.get_unit(operation, secondary, primary=False) | ||||
| @@ -48,7 +48,7 @@ | |||||
| </div> | </div> | ||||
| % if summary: | % if summary: | ||||
| <div class="summary"> | <div class="summary"> | ||||
| ${render_summary(renderer, summary, detail=True)} | |||||
| ${render_summary(renderer, summary, detail=True, sortby=sortby)} | |||||
| </div> | </div> | ||||
| % endif | % endif | ||||
| </div> | </div> | ||||
| @@ -95,9 +95,32 @@ | |||||
| </td> | </td> | ||||
| </tr> | </tr> | ||||
| </%def> | </%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: | % 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> | <h3 class="head">Kills:</h3> | ||||
| ${_build_sort_changer(["new", "old", "value"], descriptors, sortby)} | |||||
| % else: | % else: | ||||
| <div class="head">Most recent kills:</div> | <div class="head">Most recent kills:</div> | ||||
| % endif | % endif | ||||
| @@ -113,9 +136,19 @@ | |||||
| </table> | </table> | ||||
| </div> | </div> | ||||
| </%def> | </%def> | ||||
| <%def name="_collection_items(summary, detail)"> | |||||
| <%def name="_collection_items(summary, detail, sortby)"> | |||||
| % if detail: | % 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> | <h3 class="head">Items:</h3> | ||||
| ${_build_sort_changer(["value", "quantity", "price"], descriptors, sortby)} | |||||
| % else: | % else: | ||||
| <div class="head">Top items:</div> | <div class="head">Top items:</div> | ||||
| % endif | % endif | ||||
| @@ -128,10 +161,10 @@ | |||||
| </div> | </div> | ||||
| </%def> | </%def> | ||||
| <%def name="render_summary(renderer, summary, detail=False)"><% | |||||
| <%def name="render_summary(renderer, summary, detail=False, sortby=None)"><% | |||||
| if renderer == "killboard_recent": | if renderer == "killboard_recent": | ||||
| return _killboard_recent(summary, detail) | |||||
| return _killboard_recent(summary, detail, sortby) | |||||
| if renderer == "collection_items": | if renderer == "collection_items": | ||||
| return _collection_items(summary, detail) | |||||
| return _collection_items(summary, detail, sortby) | |||||
| raise RuntimeError("Unknown renderer: %s" % renderer) | raise RuntimeError("Unknown renderer: %s" % renderer) | ||||
| %></%def> | %></%def> | ||||