From 87f3764ebfa4fb8ec1512e030c6e49fcbf9323da Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 28 Feb 2026 20:56:01 -0600 Subject: [PATCH] feat: add schema, import support for `Log.locations` still need to add support for edit, export --- .../versions/3bef7d380a38_add_loglocation.py | 118 ++++++++++++++++++ src/wuttafarm/db/model/log.py | 38 ++++++ src/wuttafarm/importing/farmos.py | 38 ++++++ src/wuttafarm/normal.py | 28 +++++ src/wuttafarm/web/views/farmos/logs.py | 30 ++++- .../web/views/farmos/logs_observation.py | 12 ++ src/wuttafarm/web/views/logs.py | 23 ++-- src/wuttafarm/web/views/logs_observation.py | 2 +- 8 files changed, 279 insertions(+), 10 deletions(-) create mode 100644 src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py diff --git a/src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py b/src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py new file mode 100644 index 0000000..0ed92d9 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py @@ -0,0 +1,118 @@ +"""add LogLocation + +Revision ID: 3bef7d380a38 +Revises: f3c7e273bfa3 +Create Date: 2026-02-28 20:41:56.051847 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "3bef7d380a38" +down_revision: Union[str, None] = "f3c7e273bfa3" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # log_location + op.create_table( + "log_location", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.ForeignKeyConstraint( + ["asset_uuid"], + ["asset.uuid"], + name=op.f("fk_log_location_asset_uuid_asset"), + ), + sa.ForeignKeyConstraint( + ["log_uuid"], ["log.uuid"], name=op.f("fk_log_location_log_uuid_log") + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_location")), + ) + op.create_table( + "log_location_version", + sa.Column( + "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False + ), + sa.Column( + "log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True + ), + sa.Column( + "asset_uuid", + wuttjamaican.db.util.UUID(), + autoincrement=False, + nullable=True, + ), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "uuid", "transaction_id", name=op.f("pk_log_location_version") + ), + ) + op.create_index( + op.f("ix_log_location_version_end_transaction_id"), + "log_location_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_log_location_version_operation_type"), + "log_location_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_log_location_version_pk_transaction_id", + "log_location_version", + ["uuid", sa.literal_column("transaction_id DESC")], + unique=False, + ) + op.create_index( + "ix_log_location_version_pk_validity", + "log_location_version", + ["uuid", "transaction_id", "end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_log_location_version_transaction_id"), + "log_location_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # log_location + op.drop_index( + op.f("ix_log_location_version_transaction_id"), + table_name="log_location_version", + ) + op.drop_index( + "ix_log_location_version_pk_validity", table_name="log_location_version" + ) + op.drop_index( + "ix_log_location_version_pk_transaction_id", table_name="log_location_version" + ) + op.drop_index( + op.f("ix_log_location_version_operation_type"), + table_name="log_location_version", + ) + op.drop_index( + op.f("ix_log_location_version_end_transaction_id"), + table_name="log_location_version", + ) + op.drop_table("log_location_version") + op.drop_table("log_location") diff --git a/src/wuttafarm/db/model/log.py b/src/wuttafarm/db/model/log.py index 7234839..b770a12 100644 --- a/src/wuttafarm/db/model/log.py +++ b/src/wuttafarm/db/model/log.py @@ -175,6 +175,19 @@ class Log(model.Base): creator=lambda asset: LogAsset(asset=asset), ) + _locations = orm.relationship( + "LogLocation", + cascade="all, delete-orphan", + cascade_backrefs=False, + back_populates="log", + ) + + locations = association_proxy( + "_locations", + "asset", + creator=lambda asset: LogLocation(asset=asset), + ) + _owners = orm.relationship( "LogOwner", cascade="all, delete-orphan", @@ -219,6 +232,7 @@ def add_log_proxies(subclass): Log.make_proxy(subclass, "log", "status") Log.make_proxy(subclass, "log", "notes") Log.make_proxy(subclass, "log", "assets") + Log.make_proxy(subclass, "log", "locations") Log.make_proxy(subclass, "log", "owners") @@ -246,6 +260,30 @@ class LogAsset(model.Base): ) +class LogLocation(model.Base): + """ + Represents a "log's location relationship" from farmOS. + """ + + __tablename__ = "log_location" + __versioned__ = {} + + uuid = model.uuid_column() + + log_uuid = model.uuid_fk_column("log.uuid", nullable=False) + log = orm.relationship( + Log, + foreign_keys=log_uuid, + back_populates="_locations", + ) + + asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False) + asset = orm.relationship( + "Asset", + foreign_keys=asset_uuid, + ) + + class LogOwner(model.Base): """ Represents a "log's owner relationship" from farmOS. diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index f65ac38..44933c3 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -980,6 +980,7 @@ class LogImporterBase(FromFarmOS, ToWutta): fields.extend( [ "assets", + "locations", "owners", ] ) @@ -1006,6 +1007,12 @@ class LogImporterBase(FromFarmOS, ToWutta): (a["asset_type"], UUID(a["uuid"])) for a in data["assets"] ] + if "locations" in self.fields: + data["locations"] = [ + (asset["asset_type"], UUID(asset["uuid"])) + for asset in data["locations"] + ] + if "owners" in self.fields: data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]] @@ -1019,6 +1026,11 @@ class LogImporterBase(FromFarmOS, ToWutta): (asset.asset_type, asset.farmos_uuid) for asset in log.assets ] + if "locations" in self.fields: + data["locations"] = [ + (asset.asset_type, asset.farmos_uuid) for asset in log.locations + ] + if "owners" in self.fields: data["owners"] = [user.farmos_uuid for user in log.owners] @@ -1054,6 +1066,32 @@ class LogImporterBase(FromFarmOS, ToWutta): ) log.assets.remove(asset) + if "locations" in self.fields: + if not target_data or target_data["locations"] != source_data["locations"]: + + for key in source_data["locations"]: + asset_type, farmos_uuid = key + if not target_data or key not in target_data["locations"]: + asset = ( + self.target_session.query(model.Asset) + .filter(model.Asset.asset_type == asset_type) + .filter(model.Asset.farmos_uuid == farmos_uuid) + .one() + ) + log.locations.append(asset) + + if target_data: + for key in target_data["locations"]: + asset_type, farmos_uuid = key + if key not in source_data["locations"]: + asset = ( + self.target_session.query(model.Asset) + .filter(model.Asset.asset_type == asset_type) + .filter(model.Asset.farmos_uuid == farmos_uuid) + .one() + ) + log.locations.remove(asset) + if "owners" in self.fields: if not target_data or target_data["owners"] != source_data["owners"]: diff --git a/src/wuttafarm/normal.py b/src/wuttafarm/normal.py index ca7be39..af1ec17 100644 --- a/src/wuttafarm/normal.py +++ b/src/wuttafarm/normal.py @@ -98,6 +98,8 @@ class Normalizer(GenericHandler): asset_objects = [] quantity_objects = [] quantity_uuids = [] + location_objects = [] + location_uuids = [] owner_objects = [] owner_uuids = [] if relationships := log.get("relationships"): @@ -132,6 +134,30 @@ class Normalizer(GenericHandler): ) asset_objects.append(asset_object) + if locations := relationships.get("location"): + for location in locations["data"]: + location_uuid = location["id"] + location_uuids.append(location_uuid) + location_object = { + "uuid": location["id"], + "type": location["type"], + "asset_type": location["type"].split("--")[1], + } + if location := included.get(location_uuid): + attrs = location["attributes"] + rels = location["relationships"] + location_object.update( + { + "drupal_id": attrs["drupal_internal__id"], + "name": attrs["name"], + "is_location": attrs["is_location"], + "is_fixed": attrs["is_fixed"], + "archived": attrs["archived"], + "notes": attrs["notes"], + } + ) + location_objects.append(location_object) + if quantities := relationships.get("quantity"): for quantity in quantities["data"]: quantity_uuid = quantity["id"] @@ -194,6 +220,8 @@ class Normalizer(GenericHandler): "quick": log["attributes"]["quick"], "status": log["attributes"]["status"], "notes": notes, + "locations": location_objects, + "location_uuids": location_uuids, "owners": owner_objects, "owner_uuids": owner_uuids, } diff --git a/src/wuttafarm/web/views/farmos/logs.py b/src/wuttafarm/web/views/farmos/logs.py index fbd2a9d..e10001c 100644 --- a/src/wuttafarm/web/views/farmos/logs.py +++ b/src/wuttafarm/web/views/farmos/logs.py @@ -60,6 +60,7 @@ class LogMasterView(FarmOSMasterView): labels = { "name": "Log Name", "log_type_name": "Log Type", + "locations": "Location", "quantities": "Quantity", } @@ -69,6 +70,7 @@ class LogMasterView(FarmOSMasterView): "timestamp", "name", "assets", + "locations", "quantities", "is_group_assignment", "owners", @@ -85,18 +87,19 @@ class LogMasterView(FarmOSMasterView): "name", "timestamp", "assets", + "locations", "quantities", - "is_group_assignment", "notes", "status", "log_type_name", "owners", + "is_group_assignment", "quick", "drupal_id", ] def get_farmos_api_includes(self): - return {"log_type", "quantity", "asset", "owner"} + return {"log_type", "quantity", "asset", "location", "owner"} def get_grid_data(self, **kwargs): return ResourceData( @@ -141,6 +144,9 @@ class LogMasterView(FarmOSMasterView): # assets g.set_renderer("assets", self.render_assets_for_grid) + # locations + g.set_renderer("locations", self.render_locations_for_grid) + # quantities g.set_renderer("quantities", self.render_quantities_for_grid) @@ -165,6 +171,23 @@ class LogMasterView(FarmOSMasterView): assets.append(asset["name"]) return ", ".join(assets) + def render_locations_for_grid(self, log, field, value): + if not value: + return "" + + locations = [] + for location in value: + if self.farmos_style_grid_links: + text = location["name"] + url = self.request.route_url( + f"farmos_{location['asset_type']}_assets.view", + uuid=location["uuid"], + ) + locations.append(tags.link_to(text, url)) + else: + locations.append(text) + return ", ".join(locations) + def render_quantities_for_grid(self, log, field, value): if not value: return None @@ -212,6 +235,9 @@ class LogMasterView(FarmOSMasterView): # assets f.set_node("assets", FarmOSAssetRefs(self.request)) + # locations + f.set_node("locations", FarmOSAssetRefs(self.request)) + # quantities f.set_node("quantities", FarmOSQuantityRefs(self.request)) diff --git a/src/wuttafarm/web/views/farmos/logs_observation.py b/src/wuttafarm/web/views/farmos/logs_observation.py index ab27b5a..0193f93 100644 --- a/src/wuttafarm/web/views/farmos/logs_observation.py +++ b/src/wuttafarm/web/views/farmos/logs_observation.py @@ -41,6 +41,18 @@ class ObservationLogView(LogMasterView): farmos_log_type = "observation" farmos_refurl_path = "/logs/observation" + grid_columns = [ + "status", + "drupal_id", + "timestamp", + "name", + "assets", + "locations", + "groups", + "is_group_assignment", + "owners", + ] + def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/logs.py b/src/wuttafarm/web/views/logs.py index 626f34d..35d9451 100644 --- a/src/wuttafarm/web/views/logs.py +++ b/src/wuttafarm/web/views/logs.py @@ -99,6 +99,7 @@ class LogMasterView(WuttaFarmMasterView): labels = { "message": "Log Name", + "locations": "Location", } grid_columns = [ @@ -107,7 +108,7 @@ class LogMasterView(WuttaFarmMasterView): "timestamp", "message", "assets", - "location", + "locations", "quantity", "is_group_assignment", "owners", @@ -124,7 +125,7 @@ class LogMasterView(WuttaFarmMasterView): "message", "timestamp", "assets", - "location", + "locations", "quantity", "notes", "status", @@ -181,6 +182,9 @@ class LogMasterView(WuttaFarmMasterView): # assets g.set_renderer("assets", self.render_assets_for_grid) + # locations + g.set_renderer("locations", self.render_assets_for_grid) + # is_group_assignment g.set_renderer("is_group_assignment", "boolean") g.set_sorter("is_group_assignment", model.Log.is_group_assignment) @@ -191,17 +195,18 @@ class LogMasterView(WuttaFarmMasterView): g.set_renderer("owners", self.render_owners_for_grid) def render_assets_for_grid(self, log, field, value): + assets = getattr(log, field) if self.farmos_style_grid_links: links = [] - for asset in log.assets: + for asset in assets: url = self.request.route_url( f"{asset.asset_type}_assets.view", uuid=asset.uuid ) links.append(tags.link_to(str(asset), url)) return ", ".join(links) - return ", ".join([str(a) for a in log.assets]) + return ", ".join([str(a) for a in assets]) def render_owners_for_grid(self, log, field, value): @@ -242,9 +247,13 @@ class LogMasterView(WuttaFarmMasterView): # nb. must explicity declare value for non-standard field f.set_default("assets", log.assets) - # location + # locations if self.creating or self.editing: - f.remove("location") # TODO: need to support this + f.remove("locations") # TODO: need to support this + else: + f.set_node("locations", LogAssetRefs(self.request)) + # nb. must explicity declare value for non-standard field + f.set_default("locations", log.locations) # log_type if self.creating: @@ -355,7 +364,7 @@ class AllLogView(LogMasterView): "message", "log_type", "assets", - "location", + "locations", "quantity", "groups", "is_group_assignment", diff --git a/src/wuttafarm/web/views/logs_observation.py b/src/wuttafarm/web/views/logs_observation.py index 0485f50..6e283ae 100644 --- a/src/wuttafarm/web/views/logs_observation.py +++ b/src/wuttafarm/web/views/logs_observation.py @@ -45,7 +45,7 @@ class ObservationLogView(LogMasterView): "timestamp", "message", "assets", - "location", + "locations", "groups", "is_group_assignment", "owners",