diff --git a/src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py b/src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py new file mode 100644 index 0000000..170e3d2 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py @@ -0,0 +1,111 @@ +"""add LogGroup + +Revision ID: 74d32b4ec210 +Revises: 3bef7d380a38 +Create Date: 2026-02-28 21:35:24.125784 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "74d32b4ec210" +down_revision: Union[str, None] = "3bef7d380a38" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # log_group + op.create_table( + "log_group", + 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_group_asset_uuid_asset") + ), + sa.ForeignKeyConstraint( + ["log_uuid"], ["log.uuid"], name=op.f("fk_log_group_log_uuid_log") + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_group")), + ) + op.create_table( + "log_group_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_group_version") + ), + ) + op.create_index( + op.f("ix_log_group_version_end_transaction_id"), + "log_group_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_log_group_version_operation_type"), + "log_group_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_log_group_version_pk_transaction_id", + "log_group_version", + ["uuid", sa.literal_column("transaction_id DESC")], + unique=False, + ) + op.create_index( + "ix_log_group_version_pk_validity", + "log_group_version", + ["uuid", "transaction_id", "end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_log_group_version_transaction_id"), + "log_group_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # log_group + op.drop_index( + op.f("ix_log_group_version_transaction_id"), table_name="log_group_version" + ) + op.drop_index("ix_log_group_version_pk_validity", table_name="log_group_version") + op.drop_index( + "ix_log_group_version_pk_transaction_id", table_name="log_group_version" + ) + op.drop_index( + op.f("ix_log_group_version_operation_type"), table_name="log_group_version" + ) + op.drop_index( + op.f("ix_log_group_version_end_transaction_id"), table_name="log_group_version" + ) + op.drop_table("log_group_version") + op.drop_table("log_group") diff --git a/src/wuttafarm/db/model/log.py b/src/wuttafarm/db/model/log.py index b770a12..afa637b 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), ) + _groups = orm.relationship( + "LogGroup", + cascade="all, delete-orphan", + cascade_backrefs=False, + back_populates="log", + ) + + groups = association_proxy( + "_groups", + "asset", + creator=lambda asset: LogGroup(asset=asset), + ) + _locations = orm.relationship( "LogLocation", cascade="all, delete-orphan", @@ -232,6 +245,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", "groups") Log.make_proxy(subclass, "log", "locations") Log.make_proxy(subclass, "log", "owners") @@ -260,6 +274,30 @@ class LogAsset(model.Base): ) +class LogGroup(model.Base): + """ + Represents a "log's group relationship" from farmOS. + """ + + __tablename__ = "log_group" + __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="_groups", + ) + + asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False) + asset = orm.relationship( + "Asset", + foreign_keys=asset_uuid, + ) + + class LogLocation(model.Base): """ Represents a "log's location relationship" from farmOS. diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 44933c3..a1b539f 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -980,6 +980,7 @@ class LogImporterBase(FromFarmOS, ToWutta): fields.extend( [ "assets", + "groups", "locations", "owners", ] @@ -1007,6 +1008,11 @@ class LogImporterBase(FromFarmOS, ToWutta): (a["asset_type"], UUID(a["uuid"])) for a in data["assets"] ] + if "groups" in self.fields: + data["groups"] = [ + (asset["asset_type"], UUID(asset["uuid"])) for asset in data["groups"] + ] + if "locations" in self.fields: data["locations"] = [ (asset["asset_type"], UUID(asset["uuid"])) @@ -1026,6 +1032,11 @@ class LogImporterBase(FromFarmOS, ToWutta): (asset.asset_type, asset.farmos_uuid) for asset in log.assets ] + if "groups" in self.fields: + data["groups"] = [ + (asset.asset_type, asset.farmos_uuid) for asset in log.groups + ] + if "locations" in self.fields: data["locations"] = [ (asset.asset_type, asset.farmos_uuid) for asset in log.locations @@ -1066,6 +1077,32 @@ class LogImporterBase(FromFarmOS, ToWutta): ) log.assets.remove(asset) + if "groups" in self.fields: + if not target_data or target_data["groups"] != source_data["groups"]: + + for key in source_data["groups"]: + asset_type, farmos_uuid = key + if not target_data or key not in target_data["groups"]: + asset = ( + self.target_session.query(model.Asset) + .filter(model.Asset.asset_type == asset_type) + .filter(model.Asset.farmos_uuid == farmos_uuid) + .one() + ) + log.groups.append(asset) + + if target_data: + for key in target_data["groups"]: + asset_type, farmos_uuid = key + if key not in source_data["groups"]: + asset = ( + self.target_session.query(model.Asset) + .filter(model.Asset.asset_type == asset_type) + .filter(model.Asset.farmos_uuid == farmos_uuid) + .one() + ) + log.groups.remove(asset) + if "locations" in self.fields: if not target_data or target_data["locations"] != source_data["locations"]: @@ -1126,18 +1163,6 @@ class ActivityLogImporter(LogImporterBase): model_class = model.ActivityLog - supported_fields = [ - "farmos_uuid", - "drupal_id", - "log_type", - "message", - "timestamp", - "is_group_assignment", - "notes", - "status", - "assets", - ] - class HarvestLogImporter(LogImporterBase): """ @@ -1146,18 +1171,6 @@ class HarvestLogImporter(LogImporterBase): model_class = model.HarvestLog - supported_fields = [ - "farmos_uuid", - "drupal_id", - "log_type", - "message", - "timestamp", - "is_group_assignment", - "notes", - "status", - "assets", - ] - class MedicalLogImporter(LogImporterBase): """ @@ -1166,18 +1179,6 @@ class MedicalLogImporter(LogImporterBase): model_class = model.MedicalLog - supported_fields = [ - "farmos_uuid", - "drupal_id", - "log_type", - "message", - "timestamp", - "is_group_assignment", - "notes", - "status", - "assets", - ] - class ObservationLogImporter(LogImporterBase): """ @@ -1186,18 +1187,6 @@ class ObservationLogImporter(LogImporterBase): model_class = model.ObservationLog - supported_fields = [ - "farmos_uuid", - "drupal_id", - "log_type", - "message", - "timestamp", - "is_group_assignment", - "notes", - "status", - "assets", - ] - class QuantityImporterBase(FromFarmOS, ToWutta): """ diff --git a/src/wuttafarm/normal.py b/src/wuttafarm/normal.py index af1ec17..5c40a49 100644 --- a/src/wuttafarm/normal.py +++ b/src/wuttafarm/normal.py @@ -96,6 +96,8 @@ class Normalizer(GenericHandler): log_type_object = {} log_type_uuid = None asset_objects = [] + group_objects = [] + group_uuids = [] quantity_objects = [] quantity_uuids = [] location_objects = [] @@ -134,6 +136,30 @@ class Normalizer(GenericHandler): ) asset_objects.append(asset_object) + if groups := relationships.get("group"): + for group in groups["data"]: + group_uuid = group["id"] + group_uuids.append(group_uuid) + group_object = { + "uuid": group["id"], + "type": group["type"], + "asset_type": group["type"].split("--")[1], + } + if group := included.get(group_uuid): + attrs = group["attributes"] + rels = group["relationships"] + group_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"], + } + ) + group_objects.append(group_object) + if locations := relationships.get("location"): for location in locations["data"]: location_uuid = location["id"] @@ -214,6 +240,8 @@ class Normalizer(GenericHandler): "name": log["attributes"]["name"], "timestamp": timestamp, "assets": asset_objects, + "groups": group_objects, + "group_uuids": group_uuids, "quantities": quantity_objects, "quantity_uuids": quantity_uuids, "is_group_assignment": log["attributes"]["is_group_assignment"], diff --git a/src/wuttafarm/web/views/farmos/logs.py b/src/wuttafarm/web/views/farmos/logs.py index e10001c..d0ee388 100644 --- a/src/wuttafarm/web/views/farmos/logs.py +++ b/src/wuttafarm/web/views/farmos/logs.py @@ -87,6 +87,7 @@ class LogMasterView(FarmOSMasterView): "name", "timestamp", "assets", + "groups", "locations", "quantities", "notes", @@ -99,7 +100,7 @@ class LogMasterView(FarmOSMasterView): ] def get_farmos_api_includes(self): - return {"log_type", "quantity", "asset", "location", "owner"} + return {"log_type", "quantity", "asset", "group", "location", "owner"} def get_grid_data(self, **kwargs): return ResourceData( @@ -144,8 +145,11 @@ class LogMasterView(FarmOSMasterView): # assets g.set_renderer("assets", self.render_assets_for_grid) + # groups + g.set_renderer("groups", self.render_assets_for_grid) + # locations - g.set_renderer("locations", self.render_locations_for_grid) + g.set_renderer("locations", self.render_assets_for_grid) # quantities g.set_renderer("quantities", self.render_quantities_for_grid) @@ -160,6 +164,9 @@ class LogMasterView(FarmOSMasterView): g.set_renderer("owners", self.render_owners_for_grid) def render_assets_for_grid(self, log, field, value): + if not value: + return "" + assets = [] for asset in value: if self.farmos_style_grid_links: @@ -171,23 +178,6 @@ 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 @@ -235,6 +225,9 @@ class LogMasterView(FarmOSMasterView): # assets f.set_node("assets", FarmOSAssetRefs(self.request)) + # groups + f.set_node("groups", FarmOSAssetRefs(self.request)) + # locations f.set_node("locations", FarmOSAssetRefs(self.request)) diff --git a/src/wuttafarm/web/views/logs.py b/src/wuttafarm/web/views/logs.py index 35d9451..2679c3f 100644 --- a/src/wuttafarm/web/views/logs.py +++ b/src/wuttafarm/web/views/logs.py @@ -125,6 +125,7 @@ class LogMasterView(WuttaFarmMasterView): "message", "timestamp", "assets", + "groups", "locations", "quantity", "notes", @@ -182,6 +183,9 @@ class LogMasterView(WuttaFarmMasterView): # assets g.set_renderer("assets", self.render_assets_for_grid) + # groups + g.set_renderer("groups", self.render_assets_for_grid) + # locations g.set_renderer("locations", self.render_assets_for_grid) @@ -247,6 +251,14 @@ class LogMasterView(WuttaFarmMasterView): # nb. must explicity declare value for non-standard field f.set_default("assets", log.assets) + # groups + if self.creating or self.editing: + f.remove("groups") # TODO: need to support this + else: + f.set_node("groups", LogAssetRefs(self.request)) + # nb. must explicity declare value for non-standard field + f.set_default("groups", log.groups) + # locations if self.creating or self.editing: f.remove("locations") # TODO: need to support this