A corporation manager and dashboard for EVE Online
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. import sqlite3
  4. from flask import g
  5. from werkzeug.local import LocalProxy
  6. __all__ = ["CampaignDB"]
  7. class CampaignDB:
  8. """Database manager for internal storage for the Campaigns module."""
  9. path = None
  10. def __init__(self):
  11. if self.path is None:
  12. raise RuntimeError("CampaignDB.path not set")
  13. self._conn = sqlite3.connect(self.path)
  14. @classmethod
  15. def _get(cls):
  16. """Return the current database, or allocate a new one if necessary."""
  17. if not hasattr(g, "_campaign_db"):
  18. g._campaign_db = cls()
  19. return g._campaign_db
  20. @classmethod
  21. def pre_hook(cls):
  22. """Hook to be called before a request context.
  23. Sets up the g.campaign_db proxy.
  24. """
  25. g.campaign_db = LocalProxy(cls._get)
  26. @classmethod
  27. def post_hook(cls, exc):
  28. """Hook to be called when tearing down an application context.
  29. Closes the database if necessary.
  30. """
  31. if hasattr(g, "_campaign_db"):
  32. g._campaign_db.close()
  33. def close(self):
  34. """Close the database connection."""
  35. return self._conn.close()
  36. def check_operation(self, campaign, operation):
  37. """Return the last updated timestamp and key for the given operation.
  38. Return (None, None) if the given operation was never updated.
  39. """
  40. query = """SELECT lu_date, lu_key FROM last_updated
  41. WHERE lu_campaign = ? AND lu_operation = ?"""
  42. res = self._conn.execute(query, (campaign, operation)).fetchall()
  43. if not res:
  44. return None, None
  45. return datetime.strptime(res[0][0], "%Y-%m-%d %H:%M:%S"), res[0][1]
  46. def touch_operation(self, campaign, operation, key=None):
  47. """Mark the given operation as just updated, or add it."""
  48. with self._conn as conn:
  49. cur = conn.execute("BEGIN TRANSACTION")
  50. cur.execute("""UPDATE last_updated
  51. SET lu_date = CURRENT_TIMESTAMP, lu_key = ?
  52. WHERE lu_campaign = ? AND lu_operation = ?""", (
  53. key, campaign, operation))
  54. if cur.rowcount == 0:
  55. cur.execute("""INSERT INTO last_updated
  56. (lu_campaign, lu_operation, lu_key) VALUES (?, ?, ?)""", (
  57. campaign, operation, key))
  58. def set_overview(self, campaign, operation, primary, secondary=None):
  59. """Set overview information for this operation."""
  60. with self._conn as conn:
  61. conn.execute("""INSERT OR REPLACE INTO overview
  62. (ov_campaign, ov_operation, ov_primary, ov_secondary)
  63. VALUES (?, ?, ?, ?)""", (
  64. campaign, operation, primary, secondary))
  65. def get_overview(self, campaign, operation):
  66. """Return a 2-tuple of overview information for this operation."""
  67. query = """SELECT ov_primary, ov_secondary FROM overview
  68. WHERE ov_campaign = ? AND ov_operation = ?"""
  69. res = self._conn.execute(query, (campaign, operation)).fetchall()
  70. return tuple(res[0]) if res else (0, None)
  71. def has_kill(self, kill_id):
  72. """Return whether the database has a killmail with the given ID."""
  73. query = "SELECT 1 FROM kill WHERE kill_id = ?"
  74. res = self._conn.execute(query, (kill_id,)).fetchall()
  75. return bool(res)
  76. def add_kill(self, kill):
  77. """Insert a killmail into the database."""
  78. try:
  79. datetime.strptime(kill["killTime"], "%Y-%m-%d %H:%M:%S")
  80. except ValueError:
  81. raise RuntimeError("Invalid kill_date=%s for kill_id=%d" % (
  82. kill["killTime"], kill["killID"]))
  83. query = """INSERT OR REPLACE INTO kill (
  84. kill_id, kill_date, kill_system, kill_victim_shipid,
  85. kill_victim_charid, kill_victim_charname, kill_victim_corpid,
  86. kill_victim_corpname, kill_victim_allianceid,
  87. kill_victim_alliancename, kill_victim_factionid,
  88. kill_victim_factionname, kill_value)
  89. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
  90. victim = kill["victim"]
  91. args = (
  92. int(kill["killID"]), kill["killTime"], int(kill["solarSystemID"]),
  93. int(victim["shipTypeID"]), int(victim["characterID"]),
  94. victim["characterName"], int(victim["corporationID"]),
  95. victim["corporationName"], int(victim["allianceID"]),
  96. victim["allianceName"], int(victim["factionID"]),
  97. victim["factionName"], float(kill["zkb"]["totalValue"]))
  98. with self._conn as conn:
  99. conn.execute(query, args)
  100. def get_kill_associations(self, campaign, kill_id):
  101. """Return a list of operations associated with a campaign and kill."""
  102. query = """SELECT ok_operation FROM oper_kill
  103. WHERE ok_campaign = ? AND ok_killid = ?"""
  104. res = self._conn.execute(query, (campaign, kill_id)).fetchall()
  105. return [row[0] for row in res]
  106. def associate_kill(self, campaign, kill_id, operations):
  107. """Associate a killmail with a set of campaign/operations."""
  108. query = """INSERT OR IGNORE INTO oper_kill
  109. (ok_campaign, ok_operation, ok_killid) VALUES (?, ?, ?)"""
  110. arglist = [(campaign, op, kill_id) for op in operations]
  111. with self._conn as conn:
  112. conn.executemany(query, arglist)
  113. def count_kills(self, campaign, operation):
  114. """Return the number of matching kills and the total kill value."""
  115. query = """SELECT COUNT(*), TOTAL(kill_value)
  116. FROM oper_kill
  117. JOIN kill ON ok_killid = kill_id
  118. WHERE ok_campaign = ? AND ok_operation = ?"""
  119. res = self._conn.execute(query, (campaign, operation)).fetchall()
  120. return tuple(res[0])
  121. def get_associated_kills(self, campaign, operation, sort="new", limit=-1,
  122. offset=0):
  123. """Return a list of kills associated with a campaign/operation.
  124. Kills are returned as dictionaries, up to a limit. Use -1 for no limit.
  125. The sort should be "new" for most recent first, "old" for most recent
  126. last, or "value" for most valuable first.
  127. """
  128. sortkeys = {
  129. "new": "ok_killid DESC",
  130. "old": "ok_killid ASC",
  131. "value": "kill_value DESC, ok_killid DESC"
  132. }
  133. if sort in sortkeys:
  134. sortkey = sortkeys[sort]
  135. else:
  136. raise ValueError(sort)
  137. if not isinstance(limit, int):
  138. raise ValueError(limit)
  139. if not isinstance(offset, int):
  140. raise ValueError(offset)
  141. query = """SELECT kill_id, kill_date, kill_system, kill_victim_shipid,
  142. kill_victim_charid, kill_victim_charname, kill_victim_corpid,
  143. kill_victim_corpname, kill_victim_allianceid,
  144. kill_victim_alliancename, kill_victim_factionid,
  145. kill_victim_factionname, kill_value
  146. FROM oper_kill
  147. JOIN kill ON ok_killid = kill_id
  148. WHERE ok_campaign = ? AND ok_operation = ?
  149. ORDER BY {} LIMIT {} OFFSET {}"""
  150. qform = query.format(sortkey, limit, offset)
  151. res = self._conn.execute(qform, (campaign, operation)).fetchall()
  152. return [{
  153. "id": row[0],
  154. "date": datetime.strptime(row[1], "%Y-%m-%d %H:%M:%S"),
  155. "system": row[2],
  156. "victim": {
  157. "ship_id": row[3],
  158. "char_id": row[4],
  159. "char_name": row[5],
  160. "corp_id": row[6],
  161. "corp_name": row[7],
  162. "alliance_id": row[8],
  163. "alliance_name": row[9],
  164. "faction_id": row[10],
  165. "faction_name": row[11]
  166. },
  167. "value": row[12]
  168. } for row in res]
  169. def update_items(self, campaign, data):
  170. """Update all item details in the database for the given campaign.
  171. The data should be a multi-layered dictionary. It maps operation names
  172. to a dict that maps character IDs to a dict that maps type IDs to
  173. tuples of integer counts and float values.
  174. """
  175. with self._conn as conn:
  176. cur = conn.execute("BEGIN TRANSACTION")
  177. query = "DELETE FROM oper_item WHERE oi_campaign = ?"
  178. cur.execute(query, (campaign,))
  179. query = """INSERT INTO oper_item (
  180. oi_campaign, oi_operation, oi_character, oi_type, oi_count,
  181. oi_value)
  182. VALUES (?, ?, ?, ?, ?, ?)"""
  183. cur.executemany(query, [
  184. (campaign, operation, int(char_id), int(type_id), int(count),
  185. float(value))
  186. for operation, chars in data.items()
  187. for char_id, types in chars.items()
  188. for type_id, (count, value) in types.items()])
  189. def get_associated_items(self, campaign, operation, sort="value", limit=-1,
  190. offset=0):
  191. """Return a list of items associated with a campaign/operation.
  192. Items are returned as 2-tuples of (item_type, item_count), up to a
  193. limit. Use -1 for no limit. The sort should be "value" for most
  194. valuable first (individual item value * quantity), "quantity" for
  195. greatest quantity first, "price" for most valuable items first.
  196. """
  197. sortkeys = {
  198. "value": "total_value DESC",
  199. "quantity": "total_count DESC",
  200. "price": "(total_value / total_count) DESC"
  201. }
  202. if sort in sortkeys:
  203. sortkey = sortkeys[sort]
  204. else:
  205. raise ValueError(sort)
  206. if not isinstance(limit, int):
  207. raise ValueError(limit)
  208. if not isinstance(offset, int):
  209. raise ValueError(offset)
  210. query = """SELECT oi_type, SUM(oi_count) AS total_count,
  211. TOTAL(oi_value) as total_value
  212. FROM oper_item
  213. WHERE oi_campaign = ? AND oi_operation = ?
  214. GROUP BY oi_type ORDER BY {} LIMIT {} OFFSET {}"""
  215. qform = query.format(sortkey, limit, offset)
  216. res = self._conn.execute(qform, (campaign, operation)).fetchall()
  217. return [(type_id, count or 0, value) for type_id, count, value in res]