diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index 30f51fe6b..7c5ca3397 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -27,12 +27,11 @@ from eos.db.gamedata.group import groups_table from eos.db.util import processEager, processWhere from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, MetaGroup, AttributeInfo, MetaData +cache = {} configVal = getattr(eos.config, "gamedataCache", None) if configVal is True: def cachedQuery(amount, *keywords): def deco(function): - cache = {} - def checkAndReturn(*args, **kwargs): useCache = kwargs.pop("useCache", True) cacheKey = [] @@ -98,6 +97,34 @@ def getItem(lookfor, eager=None): return item +@cachedQuery(1, "lookfor") +def getItems(lookfor, eager=None): + """ + Gets a list of items. Does a bit of cache hackery to get working properly -- cache + is usually based on function calls with the parameters, needed to extract data directly. + Works well enough. Not currently used, but it's here for possible future inclusion + """ + + toGet = [] + results = [] + + for id in lookfor: + if (id, None) in cache: + results.append(cache.get((id, None))) + else: + toGet.append(id) + + if len(toGet) > 0: + # Get items that aren't currently cached, and store them in the cache + items = gamedata_session.query(Item).filter(Item.ID.in_(toGet)).all() + for item in items: + cache[(item.ID, None)] = item + results += items + + # sort the results based on the original indexing + results.sort(key=lambda x: lookfor.index(x.ID)) + return results + @cachedQuery(1, "lookfor") def getAlphaClone(lookfor, eager=None): if isinstance(lookfor, int): diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 9d8de9b7f..5f3c12879 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -18,7 +18,6 @@ # =============================================================================== import datetime -from sqlalchemy import inspect from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql import and_ @@ -244,7 +243,7 @@ mapper(ProjectedFit, projectedFits_table, properties={ "_ProjectedFit__amount": projectedFits_table.c.amount, } - ) +) mapper(CommandFit, commandFits_table) @@ -253,18 +252,22 @@ def rel_listener(target, value, initiator): if not target or (isinstance(value, Module) and value.isEmpty): return - print "{} has has has a relationship changes :(".format(target) + print "{} has had a relationship change :D".format(target) target.modified = datetime.datetime.now() def load_listener(target, context): - # We only want to se these events when the fit is first loaded (otherwise events will fire during the initial + # We only want to see these events when the fit is first loaded (otherwise events will fire during the initial # population of data). This sets listeners for all the relationships on fits. This allows us to update the fit's # modified date whenever something is added/removed from fit # See http://docs.sqlalchemy.org/en/rel_1_0/orm/events.html#sqlalchemy.orm.events.InstanceEvents.load - for rel in inspect(es_Fit).relationships: - listen(rel, 'append', rel_listener) - listen(rel, 'remove', rel_listener) + + # todo: when we can, move over to `inspect(es_Fit).relationships` (when mac binaries are updated) + manager = getattr(es_Fit, "_sa_class_manager", None) + if manager: + for rel in manager.mapper.relationships: + listen(rel, 'append', rel_listener) + listen(rel, 'remove', rel_listener) listen(Module, 'load', load_listener) diff --git a/eos/db/saveddata/module.py b/eos/db/saveddata/module.py index 6e4da73b4..6993e53fa 100644 --- a/eos/db/saveddata/module.py +++ b/eos/db/saveddata/module.py @@ -17,7 +17,6 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import inspect from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime, select from sqlalchemy.orm import relation, mapper from sqlalchemy.event import listen @@ -55,16 +54,20 @@ def update_fit_modified(target, value, oldvalue, initiator): target.owner.modified = datetime.datetime.now() -def my_load_listener(target, context): +def load_listener(target, context): # We only want to se these events when the module is first loaded (otherwise events will fire during the initial # population of data). This runs through all columns and sets up "set" events on each column. We do it with each # column because the alternative would be to do a before/after_update for the Mapper itself, however we're only # allowed to change the local attributes during those events as that's inter-flush. # See http://docs.sqlalchemy.org/en/rel_1_0/orm/session_events.html#mapper-level-events - for col in inspect(Module).column_attrs: - listen(col, 'set', update_fit_modified) + + # @todo replace with `inspect(Module).column_attrs` when mac binaries are updated + manager = getattr(Module, "_sa_class_manager", None) + if manager: + for col in manager.mapper.column_attrs: + listen(col, 'set', update_fit_modified) -listen(Module, 'load', my_load_listener) +listen(Module, 'load', load_listener) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 1088c1b0f..e82d2f744 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -18,6 +18,7 @@ # =============================================================================== from sqlalchemy.sql import and_ +from sqlalchemy import desc, select from eos.db import saveddata_session, sd_lock from eos.db.saveddata.fit import projectedFits_table @@ -242,6 +243,22 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None): return fits +def getRecentFits(ownerID=None, where=None, eager=None): + eager = processEager(eager) + with sd_lock: + q = select(( + Fit.ID, + Fit.shipID, + Fit.name, + Fit.modified, + Fit.created, + Fit.timestamp + )).order_by(desc(Fit.modified), desc(Fit.timestamp)).limit(50) + fits = eos.db.saveddata_session.execute(q).fetchall() + + return fits + + def getFitsWithModules(typeIDs, eager=None): """ Get all the fits that have typeIDs fitted to them diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 5e1ad08c6..8ef05641c 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -20,6 +20,7 @@ import sys import os.path from logbook import Logger +import datetime import sqlalchemy # noinspection PyPackageRequirements @@ -919,7 +920,16 @@ class MainFrame(wx.Frame): wx.PostEvent(self, FitSelected(fitID=fit.ID)) wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True)) else: - wx.PostEvent(self.shipBrowser, ImportSelected(fits=fits, back=True)) + fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) + results = [] + for fit in fits: + results.append(( + fit.ID, + fit.name, + fit.modified or fit.created or datetime.datetime.fromtimestamp(fit.timestamp), + fit.ship.item + )) + wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True)) def closeProgressDialog(self): # Windows apparently handles ProgressDialogs differently. We can diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index 59cbd96b9..78f12b26b 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -338,15 +338,21 @@ class NavigationPanel(SFItem.SFBrowserItem): self.newBmpH = BitmapLoader.getBitmap("fit_add_small", "gui") self.resetBmpH = BitmapLoader.getBitmap("freset_small", "gui") self.switchBmpH = BitmapLoader.getBitmap("fit_switch_view_mode_small", "gui") + self.recentBmpH = BitmapLoader.getBitmap("frecent_small", "gui") switchImg = BitmapLoader.getImage("fit_switch_view_mode_small", "gui") switchImg = switchImg.AdjustChannels(1, 1, 1, 0.4) self.switchBmpD = wx.BitmapFromImage(switchImg) + recentImg = BitmapLoader.getImage("frecent_small", "gui") + recentImg = recentImg.AdjustChannels(1, 1, 1, 0.4) + self.recentBmpD = wx.BitmapFromImage(recentImg) + self.resetBmp = self.AdjustChannels(self.resetBmpH) self.rewBmp = self.AdjustChannels(self.rewBmpH) self.searchBmp = self.AdjustChannels(self.searchBmpH) self.switchBmp = self.AdjustChannels(self.switchBmpH) + self.recentBmp = self.AdjustChannels(self.recentBmpH) self.newBmp = self.AdjustChannels(self.newBmpH) self.toolbar.AddButton(self.resetBmp, "Ship groups", clickCallback=self.OnHistoryReset, @@ -357,6 +363,9 @@ class NavigationPanel(SFItem.SFBrowserItem): self.btnSwitch = self.toolbar.AddButton(self.switchBmpD, "Hide empty ship groups", clickCallback=self.ToggleEmptyGroupsView, hoverBitmap=self.switchBmpH, show=False) + self.btnRecent = self.toolbar.AddButton(self.recentBmpD, "Recent Fits", + clickCallback=self.ToggleRecentShips, hoverBitmap=self.recentBmpH, + show=True) modifier = "CTRL" if 'wxMac' not in wx.PlatformInfo else "CMD" self.toolbar.AddButton(self.searchBmp, "Search fittings ({}+F)".format(modifier), clickCallback=self.ToggleSearchBox, @@ -416,6 +425,27 @@ class NavigationPanel(SFItem.SFBrowserItem): def OnResize(self, event): self.Refresh() + def ToggleRecentShips(self, bool = None, emitEvent = True): + # this is so janky. Need to revaluate pretty much entire ship browser. >.< + toggle = bool if bool is not None else not self.shipBrowser.recentFits + + if not toggle: + self.shipBrowser.recentFits = False + self.btnRecent.label = "Recent Fits" + self.btnRecent.normalBmp = self.recentBmpD + + if emitEvent: + wx.PostEvent(self.shipBrowser, Stage1Selected()) + else: + self.shipBrowser.recentFits = True + self.btnRecent.label = "Hide Recent Fits" + self.btnRecent.normalBmp = self.recentBmp + + if emitEvent: + sFit = Fit.getInstance() + fits = sFit.getRecentFits() + wx.PostEvent(self.shipBrowser, ImportSelected(fits=fits, back=True, recent=True)) + def ToggleEmptyGroupsView(self): if self.shipBrowser.filterShipsWithNoFits: self.shipBrowser.filterShipsWithNoFits = False @@ -454,11 +484,13 @@ class NavigationPanel(SFItem.SFBrowserItem): wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID)) def OnHistoryReset(self): + self.ToggleRecentShips(False, False) if self.shipBrowser.browseHist: self.shipBrowser.browseHist = [] self.gotoStage(1, 0) def OnHistoryBack(self): + self.ToggleRecentShips(False, False) if len(self.shipBrowser.browseHist) > 0: stage, data = self.shipBrowser.browseHist.pop() self.gotoStage(stage, data) @@ -538,6 +570,7 @@ class NavigationPanel(SFItem.SFBrowserItem): self.bkBitmap.mFactor = mFactor def gotoStage(self, stage, data=None): + self.shipBrowser.recentFits = False if stage == 1: wx.PostEvent(self.Parent, Stage1Selected()) elif stage == 2: @@ -573,6 +606,7 @@ class ShipBrowser(wx.Panel): self._stage3ShipName = "" self.fitIDMustEditName = -1 self.filterShipsWithNoFits = False + self.recentFits = False self.racesFilter = {} @@ -629,7 +663,8 @@ class ShipBrowser(wx.Panel): def RefreshList(self, event): stage = self.GetActiveStage() - if stage == 3 or stage == 4: + + if stage in (3, 4, 5): self.lpane.RefreshList(True) event.Skip() @@ -672,6 +707,7 @@ class ShipBrowser(wx.Panel): return self.racesFilter[race] def stage1(self, event): + self.navpanel.ToggleRecentShips(False, False) self._lastStage = self._activeStage self._activeStage = 1 self.lastdata = 0 @@ -727,6 +763,7 @@ class ShipBrowser(wx.Panel): def stage2Callback(self, data): if self.GetActiveStage() != 2: return + self.navpanel.ToggleRecentShips(False, False) categoryID = self._stage2Data ships = list(data[1]) @@ -812,7 +849,7 @@ class ShipBrowser(wx.Panel): return info[1] def stage3(self, event): - + self.navpanel.ToggleRecentShips(False, False) self.lpane.ShowLoading(False) # If back is False, do not append to history. This could be us calling @@ -921,6 +958,10 @@ class ShipBrowser(wx.Panel): self.Layout() def importStage(self, event): + """ + The import stage handles both displaying fits after importing as well as displaying recent fits. todo: need to + reconcile these two better into a more uniform function, right now hacked together to get working + """ self.lpane.ShowLoading(False) self.navpanel.ShowNewFitButton(False) @@ -934,29 +975,26 @@ class ShipBrowser(wx.Panel): fits = event.fits - # sort by ship name, then fit name - fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) - self.lastdata = fits self.lpane.Freeze() self.lpane.RemoveAllChildren() if fits: for fit in fits: - shipTrait = fit.ship.item.traits.traitText if (fit.ship.item.traits is not None) else "" - # empty string if no traits + shipItem = fit[3] + shipTrait = shipItem.traits.traitText if (shipItem.traits is not None) else "" self.lpane.AddWidget(FitItem( self.lpane, - fit.ID, + fit[0], ( - fit.ship.item.name, + shipItem.name, shipTrait, - fit.name, - fit.booster, - fit.modified, + fit[1], + False, + fit[2] ), - fit.ship.item.ID, + shipItem.ID, )) self.lpane.RefreshList(doFocus=False) self.lpane.Thaw() @@ -1534,7 +1572,6 @@ class FitItem(SFItem.SFBrowserItem): self.tcFitName.Bind(wx.EVT_TEXT_ENTER, self.renameFit) self.tcFitName.Bind(wx.EVT_KILL_FOCUS, self.editLostFocus) self.tcFitName.Bind(wx.EVT_KEY_DOWN, self.editCheckEsc) - self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnMouseCaptureLost) self.animTimerId = wx.NewId() @@ -1567,13 +1604,6 @@ class FitItem(SFItem.SFBrowserItem): self.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) self.Bind(wx.EVT_MIDDLE_UP, self.OpenNewTab) - def OnFitChanged(self, evt): - if evt.fitID == self.fitID: - sFit = Fit.getInstance() - fit = sFit.getFit(evt.fitID) - if fit.modified: - self.timestamp = fit.modified - def OpenNewTab(self, evt): self.selectFit(newTab=True) @@ -1960,6 +1990,15 @@ class FitItem(SFItem.SFBrowserItem): state = SFItem.SB_ITEM_NORMAL return state + def Refresh(self): + activeFit = self.mainFrame.getActiveFit() + if activeFit == self.fitID: + sFit = Fit.getInstance() + fit = sFit.getFit(activeFit) + self.timestamp = fit.modified + + SFItem.SFBrowserItem.Refresh(self) + def RenderBackground(self): rect = self.GetRect() diff --git a/imgs/gui/frecent_small.png b/imgs/gui/frecent_small.png new file mode 100644 index 000000000..911da3f1d Binary files /dev/null and b/imgs/gui/frecent_small.png differ diff --git a/service/fit.py b/service/fit.py index 112a790c9..4d2ac94ca 100644 --- a/service/fit.py +++ b/service/fit.py @@ -98,6 +98,20 @@ class Fit(object): return names + @staticmethod + def getRecentFits(): + """ Fetches recently modified fits, used with shipBrowser """ + pyfalog.debug("Fetching recent fits") + fits = eos.db.getRecentFits() + returnInfo = [] + + for fit in fits: + item = eos.db.getItem(fit[1]) + returnInfo.append((fit[0], fit[2], fit[3] or fit[4] or datetime.datetime.fromtimestamp(fit[5]), item)) + # ID name timestamps + + return returnInfo + @staticmethod def getFitsWithModules(typeIDs): """ Lists fits flagged as booster """