diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index d0ca8a5657..7ea1b1f7c1 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -27,12 +27,11 @@ 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/booster.py b/eos/db/saveddata/booster.py index 518bca0030..e40762cd91 100644 --- a/eos/db/saveddata/booster.py +++ b/eos/db/saveddata/booster.py @@ -20,7 +20,7 @@ from sqlalchemy import Table, Column, ForeignKey, Integer, Boolean, DateTime from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import mapper, relation -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.booster import Booster @@ -30,8 +30,8 @@ Column("itemID", Integer), Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False), Column("active", Boolean), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), ) # Legacy booster side effect code, should disable but a mapper relies on it. diff --git a/eos/db/saveddata/cargo.py b/eos/db/saveddata/cargo.py index 83ab0ca0b8..b9b23adaf7 100644 --- a/eos/db/saveddata/cargo.py +++ b/eos/db/saveddata/cargo.py @@ -18,19 +18,24 @@ # =============================================================================== from sqlalchemy import Table, Column, Integer, ForeignKey, DateTime -from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +from sqlalchemy.orm import mapper, relation +import datetime from eos.db import saveddata_meta from eos.saveddata.cargo import Cargo +from eos.saveddata.fit import Fit cargo_table = Table("cargo", saveddata_meta, Column("ID", Integer, primary_key=True), Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False, index=True), Column("itemID", Integer, nullable=False), Column("amount", Integer, nullable=False), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), ) -mapper(Cargo, cargo_table) +mapper(Cargo, cargo_table, + properties={ + "owner": relation(Fit) + } +) diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index b96ca93ccf..599f079b3b 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime, Float from sqlalchemy.orm import relation, mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.db.saveddata.implant import charImplants_table @@ -39,8 +39,8 @@ Column("alphaCloneID", Integer, nullable=True), Column("ownerID", ForeignKey("users.ID"), nullable=True), Column("secStatus", Float, nullable=True, default=0.0), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now())) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) mapper(Character, characters_table, properties={ diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py index fff78a3ee0..28f77a9836 100644 --- a/eos/db/saveddata/crest.py +++ b/eos/db/saveddata/crest.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, String, DateTime from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.crestchar import CrestChar @@ -29,6 +29,6 @@ Column("name", String, nullable=False, unique=True), Column("refresh_token", String, nullable=False), # These records aren't updated. Instead, they are dropped and created, hence we don't have a modified field - Column("created", DateTime, nullable=True, default=func.now())) + Column("created", DateTime, nullable=True, default=datetime.datetime.now)) mapper(CrestChar, crest_table) diff --git a/eos/db/saveddata/damagePattern.py b/eos/db/saveddata/damagePattern.py index 6f20cd92f7..8a25367036 100644 --- a/eos/db/saveddata/damagePattern.py +++ b/eos/db/saveddata/damagePattern.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.damagePattern import DamagePattern @@ -32,8 +32,8 @@ Column("kineticAmount", Integer), Column("explosiveAmount", Integer), Column("ownerID", ForeignKey("users.ID"), nullable=True), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) mapper(DamagePattern, damagePatterns_table) diff --git a/eos/db/saveddata/drone.py b/eos/db/saveddata/drone.py index 93efd99562..b4c0cefb97 100644 --- a/eos/db/saveddata/drone.py +++ b/eos/db/saveddata/drone.py @@ -18,11 +18,12 @@ # =============================================================================== from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime -from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +from sqlalchemy.orm import mapper, relation +import datetime from eos.db import saveddata_meta from eos.saveddata.drone import Drone +from eos.saveddata.fit import Fit drones_table = Table("drones", saveddata_meta, Column("groupID", Integer, primary_key=True), @@ -31,8 +32,12 @@ Column("amount", Integer, nullable=False), Column("amountActive", Integer, nullable=False), Column("projected", Boolean, default=False), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) -mapper(Drone, drones_table) +mapper(Drone, drones_table, + properties={ + "owner": relation(Fit) + } +) diff --git a/eos/db/saveddata/fighter.py b/eos/db/saveddata/fighter.py index e20e9fbdf6..bb1ed133fe 100644 --- a/eos/db/saveddata/fighter.py +++ b/eos/db/saveddata/fighter.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime from sqlalchemy.orm import mapper, relation -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.fighterAbility import FighterAbility @@ -33,8 +33,8 @@ Column("active", Boolean, nullable=True), Column("amount", Integer, nullable=False), Column("projected", Boolean, default=False), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) fighter_abilities_table = Table("fightersAbilities", saveddata_meta, diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 30f323ee95..7f8fdbe616 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -22,7 +22,7 @@ from sqlalchemy.sql import and_ from sqlalchemy.orm import relation, reconstructor, mapper, relationship from sqlalchemy import ForeignKey, Column, Integer, String, Table, Boolean, DateTime -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.db import saveddata_session @@ -59,8 +59,8 @@ Column("implantLocation", Integer, nullable=False, default=ImplantLocation.FIT), Column("notes", String, nullable=True), Column("ignoreRestrictions", Boolean, default=0), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now) ) projectedFits_table = Table("projectedFits", saveddata_meta, @@ -68,16 +68,16 @@ Column("victimID", ForeignKey("fits.ID"), primary_key=True), Column("amount", Integer, nullable=False, default=1), Column("active", Boolean, nullable=False, default=1), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) commandFits_table = Table("commandFits", saveddata_meta, Column("boosterID", ForeignKey("fits.ID"), primary_key=True), Column("boostedID", ForeignKey("fits.ID"), primary_key=True), Column("active", Boolean, nullable=False, default=1), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) @@ -143,49 +143,70 @@ def __repr__(self): "booster_fit", # .. and return the booster fit creator=lambda boosterID, booster_fit: CommandFit(boosterID, booster_fit) ) + + +# These relationships are broken out so that we can easily access it in the events stuff +# We sometimes don't want particular relationships to cause a fit modified update (eg: projecting +# a fit onto another would 'modify' both fits unless the following relationship is ignored) +projectedFitSourceRel = relationship( + ProjectedFit, + primaryjoin=projectedFits_table.c.sourceID == fits_table.c.ID, + backref='source_fit', + collection_class=attribute_mapped_collection('victimID'), + cascade='all, delete, delete-orphan') + + +boostedOntoRel = relationship( + CommandFit, + primaryjoin=commandFits_table.c.boosterID == fits_table.c.ID, + backref='booster_fit', + collection_class=attribute_mapped_collection('boostedID'), + cascade='all, delete, delete-orphan') + mapper(es_Fit, fits_table, properties={ - "_Fit__modules" : relation( + "_Fit__modules": relation( Module, collection_class=HandledModuleList, primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), # noqa order_by=modules_table.c.position, cascade='all, delete, delete-orphan'), - "_Fit__projectedModules" : relation( + "_Fit__projectedModules": relation( Module, collection_class=HandledProjectedModList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), # noqa - "owner" : relation( + "owner": relation( User, backref="fits"), - "itemID" : fits_table.c.shipID, - "shipID" : fits_table.c.shipID, - "_Fit__boosters" : relation( + "itemID": fits_table.c.shipID, + "shipID": fits_table.c.shipID, + "_Fit__boosters": relation( Booster, collection_class=HandledImplantBoosterList, cascade='all, delete, delete-orphan', + backref='owner', single_parent=True), - "_Fit__drones" : relation( + "_Fit__drones": relation( Drone, collection_class=HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), # noqa - "_Fit__fighters" : relation( + "_Fit__fighters": relation( Fighter, collection_class=HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == False)), # noqa - "_Fit__cargo" : relation( + "_Fit__cargo": relation( Cargo, collection_class=HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin=and_(cargo_table.c.fitID == fits_table.c.ID)), - "_Fit__projectedDrones" : relation( + "_Fit__projectedDrones": relation( Drone, collection_class=HandledProjectedDroneList, cascade='all, delete, delete-orphan', @@ -197,51 +218,41 @@ def __repr__(self): cascade='all, delete, delete-orphan', single_parent=True, primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == True)), # noqa - "_Fit__implants" : relation( + "_Fit__implants": relation( Implant, collection_class=HandledImplantBoosterList, cascade='all, delete, delete-orphan', - backref='fit', + backref='owner', single_parent=True, primaryjoin=fitImplants_table.c.fitID == fits_table.c.ID, secondaryjoin=fitImplants_table.c.implantID == Implant.ID, secondary=fitImplants_table), - "_Fit__character" : relation( + "_Fit__character": relation( Character, backref="fits"), - "_Fit__damagePattern" : relation(DamagePattern), - "_Fit__targetResists" : relation(TargetResists), - "projectedOnto" : relationship( - ProjectedFit, - primaryjoin=projectedFits_table.c.sourceID == fits_table.c.ID, - backref='source_fit', - collection_class=attribute_mapped_collection('victimID'), - cascade='all, delete, delete-orphan'), - "victimOf" : relationship( + "_Fit__damagePattern": relation(DamagePattern), + "_Fit__targetResists": relation(TargetResists), + "projectedOnto": projectedFitSourceRel, + "victimOf": relationship( ProjectedFit, primaryjoin=fits_table.c.ID == projectedFits_table.c.victimID, backref='victim_fit', collection_class=attribute_mapped_collection('sourceID'), cascade='all, delete, delete-orphan'), - "boostedOnto" : relationship( - CommandFit, - primaryjoin=commandFits_table.c.boosterID == fits_table.c.ID, - backref='booster_fit', - collection_class=attribute_mapped_collection('boostedID'), - cascade='all, delete, delete-orphan'), - "boostedOf" : relationship( + "boostedOnto": boostedOntoRel, + "boostedOf": relationship( CommandFit, primaryjoin=fits_table.c.ID == commandFits_table.c.boostedID, backref='boosted_fit', collection_class=attribute_mapped_collection('boosterID'), cascade='all, delete, delete-orphan'), } - ) +) mapper(ProjectedFit, projectedFits_table, properties={ "_ProjectedFit__amount": projectedFits_table.c.amount, } - ) +) mapper(CommandFit, commandFits_table) diff --git a/eos/db/saveddata/implant.py b/eos/db/saveddata/implant.py index e1f1632c4c..edf9aac12d 100644 --- a/eos/db/saveddata/implant.py +++ b/eos/db/saveddata/implant.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.implant import Implant @@ -28,8 +28,8 @@ Column("ID", Integer, primary_key=True), Column("itemID", Integer), Column("active", Boolean), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) fitImplants_table = Table("fitImplants", saveddata_meta, diff --git a/eos/db/saveddata/implantSet.py b/eos/db/saveddata/implantSet.py index c32700740c..369f047602 100644 --- a/eos/db/saveddata/implantSet.py +++ b/eos/db/saveddata/implantSet.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, String, DateTime from sqlalchemy.orm import relation, mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.db.saveddata.implant import implantsSetMap_table @@ -30,8 +30,8 @@ implant_set_table = Table("implantSets", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) mapper(ImplantSet, implant_set_table, diff --git a/eos/db/saveddata/module.py b/eos/db/saveddata/module.py index 7f869c0a6e..704eca99d4 100644 --- a/eos/db/saveddata/module.py +++ b/eos/db/saveddata/module.py @@ -17,9 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime +from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime, select from sqlalchemy.orm import relation, mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.module import Module @@ -34,9 +34,12 @@ Column("state", Integer, CheckConstraint("state >= -1"), CheckConstraint("state <= 2")), Column("projected", Boolean, default=False, nullable=False), Column("position", Integer), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"')) mapper(Module, modules_table, properties={"owner": relation(Fit)}) + + + diff --git a/eos/db/saveddata/override.py b/eos/db/saveddata/override.py index ed32884054..aa99c9763c 100644 --- a/eos/db/saveddata/override.py +++ b/eos/db/saveddata/override.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, Float, DateTime from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.override import Override @@ -28,8 +28,8 @@ Column("itemID", Integer, primary_key=True, index=True), Column("attrID", Integer, primary_key=True, index=True), Column("value", Float, nullable=False), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) mapper(Override, overrides_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 1088c1b0f4..e82d2f744a 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/eos/db/saveddata/skill.py b/eos/db/saveddata/skill.py index 40192def95..5e56f2cdbe 100644 --- a/eos/db/saveddata/skill.py +++ b/eos/db/saveddata/skill.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, ForeignKey, DateTime from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.character import Skill @@ -29,8 +29,8 @@ Column("characterID", ForeignKey("characters.ID"), primary_key=True, index=True), Column("itemID", Integer, primary_key=True), Column("_Skill__level", Integer, nullable=True), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) mapper(Skill, skills_table) diff --git a/eos/db/saveddata/targetResists.py b/eos/db/saveddata/targetResists.py index bda30405d4..f100bc2ddd 100644 --- a/eos/db/saveddata/targetResists.py +++ b/eos/db/saveddata/targetResists.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String, DateTime from sqlalchemy.orm import mapper -import sqlalchemy.sql.functions as func +import datetime from eos.db import saveddata_meta from eos.saveddata.targetResists import TargetResists @@ -32,8 +32,8 @@ Column("kineticAmount", Float), Column("explosiveAmount", Float), Column("ownerID", ForeignKey("users.ID"), nullable=True), - Column("created", DateTime, nullable=True, default=func.now()), - Column("modified", DateTime, nullable=True, onupdate=func.now()) + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) mapper(TargetResists, targetResists_table) diff --git a/eos/events.py b/eos/events.py new file mode 100644 index 0000000000..dd7a88a2b4 --- /dev/null +++ b/eos/events.py @@ -0,0 +1,86 @@ +# Decided to put this in it's own file so that we can easily choose not to import it (thanks to mac-deprecated builds =/) + +import datetime +from sqlalchemy.event import listen +from sqlalchemy.orm.collections import InstrumentedList + +from eos.db.saveddata.fit import projectedFitSourceRel, boostedOntoRel + +from eos.saveddata.fit import Fit +from eos.saveddata.module import Module +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.cargo import Cargo +from eos.saveddata.implant import Implant +from eos.saveddata.booster import Booster + +ignored_rels = [ + projectedFitSourceRel, + boostedOntoRel +] + + +def update_fit_modified(target, value, oldvalue, initiator): + if not target.owner: + return + + if value != oldvalue: + # some things (like Implants) have a backref to the fit, which actually produces a list. + # In this situation, simply take the 0 index to get to the fit. + # There may be cases in the future in which there are multiple fits, so this should be + # looked at more indepth later + if isinstance(target.owner, InstrumentedList): + parent = target.owner[0] + else: + parent = target.owner + + # ensure this is a fit we're dealing with + if isinstance(parent, Fit): + parent.modified = datetime.datetime.now() + + +def apply_col_listeners(target, context): + # We only want to set 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 + + # @todo replace with `inspect(Module).column_attrs` when mac binaries are updated + + manager = getattr(target.__class__, "_sa_class_manager", None) + if manager: + for col in manager.mapper.column_attrs: + listen(col, 'set', update_fit_modified) + + +def rel_listener(target, value, initiator): + if not target or (isinstance(value, Module) and value.isEmpty): + return + + print "{} has had a relationship change :D".format(target) + target.modified = datetime.datetime.now() + + +def apply_rel_listeners(target, context): + # 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 + + # todo: when we can, move over to `inspect(es_Fit).relationships` (when mac binaries are updated) + manager = getattr(target.__class__, "_sa_class_manager", None) + if manager: + for rel in manager.mapper.relationships: + if rel in ignored_rels: + continue + listen(rel, 'append', rel_listener) + listen(rel, 'remove', rel_listener) + +listen(Fit, 'load', apply_rel_listeners) +listen(Module, 'load', apply_col_listeners) +listen(Drone, 'load', apply_col_listeners) +listen(Fighter, 'load', apply_col_listeners) +listen(Cargo, 'load', apply_col_listeners) +listen(Implant, 'load', apply_col_listeners) +listen(Booster, 'load', apply_col_listeners) \ No newline at end of file diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index d376820b7c..83ce47f840 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -21,6 +21,7 @@ from copy import deepcopy from itertools import chain from math import sqrt, log, asinh +import datetime from sqlalchemy.orm import validates, reconstructor @@ -77,6 +78,8 @@ def __init__(self, ship=None, name=""): self.projected = False self.name = name self.timestamp = time.time() + self.created = None + self.modified = None self.modeID = None self.build() @@ -180,6 +183,14 @@ def mode(self, mode): self.__mode = mode self.modeID = mode.item.ID if mode is not None else None + @property + def modifiedCoalesce(self): + """ + This is a property that should get whichever date is available for the fit. @todo: migrate old timestamp data + and ensure created / modified are set in database to get rid of this + """ + return self.modified or self.created or datetime.datetime.fromtimestamp(self.timestamp) + @property def character(self): return self.__character if self.__character is not None else Character.getAll0() diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 6b67ebcdd6..b7c9254976 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 @@ -940,7 +941,16 @@ def _openAfterImport(self, fits): 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.modifiedCoalesce, + 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 4fe421082e..54fd39f9cc 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -337,15 +337,21 @@ def __init__(self, parent, size=(-1, 24)): 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, @@ -356,6 +362,9 @@ def __init__(self, parent, size=(-1, 24)): 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, @@ -415,6 +424,27 @@ def OnBrowserSearchBoxEsc(self, event): 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 @@ -453,11 +483,13 @@ def OnNewFitting(self): 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) @@ -537,6 +569,7 @@ def RenderBackground(self): 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: @@ -572,6 +605,7 @@ def __init__(self, parent): self._stage3ShipName = "" self.fitIDMustEditName = -1 self.filterShipsWithNoFits = False + self.recentFits = False self.racesFilter = {} @@ -628,7 +662,8 @@ def RefreshContent(self): 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() @@ -671,6 +706,7 @@ def GetRaceFilterState(self, race): return self.racesFilter[race] def stage1(self, event): + self.navpanel.ToggleRecentShips(False, False) self._lastStage = self._activeStage self._activeStage = 1 self.lastdata = 0 @@ -726,6 +762,7 @@ def raceNameKey(self, ship): def stage2Callback(self, data): if self.GetActiveStage() != 2: return + self.navpanel.ToggleRecentShips(False, False) categoryID = self._stage2Data ships = list(data[1]) @@ -811,7 +848,7 @@ def nameKey(info): 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 @@ -920,6 +957,10 @@ def searchStage(self, event): 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) @@ -933,29 +974,26 @@ def importStage(self, event): 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.timestamp, + fit[1], + False, + fit[2] ), - fit.ship.item.ID, + shipItem.ID, )) self.lpane.RefreshList(doFocus=False) self.lpane.Thaw() @@ -1473,6 +1511,7 @@ def __init__(self, parent, fitID=None, shipFittingInfo=("Test", "TestTrait", "cn self.shipFittingInfo = shipFittingInfo self.shipName, self.shipTrait, self.fitName, self.fitBooster, self.timestamp = shipFittingInfo + self.shipTrait = re.sub("<.*?>", " ", self.shipTrait) # see GH issue #62 @@ -1553,10 +1592,13 @@ def __init__(self, parent, fitID=None, shipFittingInfo=("Test", "TestTrait", "cn # self.animCount = 0 # ===================================================================== + """ + # Remove this bit as the time stuff is non-functional (works... but not exactly sure what it's meant to do) self.selTimerID = wx.NewId() self.selTimer = wx.Timer(self, self.selTimerID) self.selTimer.Start(100) + """ self.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) self.Bind(wx.EVT_MIDDLE_UP, self.OpenNewTab) @@ -1645,6 +1687,7 @@ def GetType(self): def OnTimer(self, event): + # @todo: figure out what exactly this is supposed to accomplish if self.selTimerID == event.GetId(): ctimestamp = time.time() interval = 5 @@ -1726,7 +1769,6 @@ def renameFit(self, event=None): self.fitName = fitName sFit.renameFit(self.fitID, self.fitName) wx.PostEvent(self.mainFrame, FitRenamed(fitID=self.fitID)) - self.Refresh() else: self.tcFitName.SetValue(self.fitName) @@ -1893,8 +1935,8 @@ def DrawItem(self, mdc): mdc.SetFont(self.fontNormal) - fitDate = time.localtime(self.timestamp) - fitLocalDate = "%d/%02d/%02d %02d:%02d" % (fitDate[0], fitDate[1], fitDate[2], fitDate[3], fitDate[4]) + fitDate = self.timestamp.strftime("%m/%d/%Y %H:%M") + fitLocalDate = fitDate #"%d/%02d/%02d %02d:%02d" % (fitDate[0], fitDate[1], fitDate[2], fitDate[3], fitDate[4]) pfdate = drawUtils.GetPartialText(mdc, fitLocalDate, self.toolbarx - self.textStartx - self.padding * 2 - self.thoverw) @@ -1947,6 +1989,15 @@ def GetState(self): 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.modifiedCoalesce + + 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 0000000000..911da3f1d3 Binary files /dev/null and b/imgs/gui/frecent_small.png differ diff --git a/pyfa.py b/pyfa.py index ff6b0a2b62..3b7426b5d5 100755 --- a/pyfa.py +++ b/pyfa.py @@ -337,6 +337,7 @@ def handleGUIException(exc_type, exc_value, exc_traceback): else: saVersion = sqlalchemy.__version__ saMatch = re.match("([0-9]+).([0-9]+)([b\.])([0-9]+)", saVersion) + config.saVersion = (int(saMatch.group(1)), int(saMatch.group(2)), int(saMatch.group(4))) if saMatch: saMajor = int(saMatch.group(1)) saMinor = int(saMatch.group(2)) @@ -362,6 +363,12 @@ def handleGUIException(exc_type, exc_value, exc_traceback): raise PreCheckException("Cannot import requests. You can download requests from https://pypi.python.org/pypi/requests.") import eos.db + + if config.saVersion[0] > 0 or config.saVersion[1] >= 7: + # <0.7 doesn't have support for events ;_; (mac-deprecated) + config.sa_events = True + import eos.events + # noinspection PyUnresolvedReferences import service.prefetch # noqa: F401 diff --git a/service/fit.py b/service/fit.py index df9fbf6ff1..3b5d4d57d2 100644 --- a/service/fit.py +++ b/service/fit.py @@ -20,6 +20,7 @@ import copy from logbook import Logger from time import time +import datetime import eos.db from eos.saveddata.booster import Booster as es_Booster @@ -93,10 +94,24 @@ def getFitsWithShip(shipID): fits = eos.db.getFitsWithShip(shipID) names = [] for fit in fits: - names.append((fit.ID, fit.name, fit.booster, fit.timestamp)) + names.append((fit.ID, fit.name, fit.booster, fit.modified or fit.created or datetime.datetime.fromtimestamp(fit.timestamp))) 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 """ @@ -258,10 +273,15 @@ def searchFits(name): pyfalog.debug("Searching for fit: {0}", name) results = eos.db.searchFits(name) fits = [] + for fit in results: fits.append(( - fit.ID, fit.name, fit.ship.item.ID, fit.ship.item.name, fit.booster, - fit.timestamp)) + fit.ID, + fit.name, + fit.ship.item.ID, + fit.ship.item.name, + fit.booster, + fit.modifiedCoalesce)) return fits def addImplant(self, fitID, itemID, recalc=True):