feat: add schema, import support for Log.groups

This commit is contained in:
Lance Edgar 2026-02-28 21:43:03 -06:00
parent 87f3764ebf
commit 1d877545ae
6 changed files with 238 additions and 67 deletions

View file

@ -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")

View file

@ -175,6 +175,19 @@ class Log(model.Base):
creator=lambda asset: LogAsset(asset=asset), 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( _locations = orm.relationship(
"LogLocation", "LogLocation",
cascade="all, delete-orphan", cascade="all, delete-orphan",
@ -232,6 +245,7 @@ def add_log_proxies(subclass):
Log.make_proxy(subclass, "log", "status") Log.make_proxy(subclass, "log", "status")
Log.make_proxy(subclass, "log", "notes") Log.make_proxy(subclass, "log", "notes")
Log.make_proxy(subclass, "log", "assets") Log.make_proxy(subclass, "log", "assets")
Log.make_proxy(subclass, "log", "groups")
Log.make_proxy(subclass, "log", "locations") Log.make_proxy(subclass, "log", "locations")
Log.make_proxy(subclass, "log", "owners") 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): class LogLocation(model.Base):
""" """
Represents a "log's location relationship" from farmOS. Represents a "log's location relationship" from farmOS.

View file

@ -980,6 +980,7 @@ class LogImporterBase(FromFarmOS, ToWutta):
fields.extend( fields.extend(
[ [
"assets", "assets",
"groups",
"locations", "locations",
"owners", "owners",
] ]
@ -1007,6 +1008,11 @@ class LogImporterBase(FromFarmOS, ToWutta):
(a["asset_type"], UUID(a["uuid"])) for a in data["assets"] (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: if "locations" in self.fields:
data["locations"] = [ data["locations"] = [
(asset["asset_type"], UUID(asset["uuid"])) (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 (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: if "locations" in self.fields:
data["locations"] = [ data["locations"] = [
(asset.asset_type, asset.farmos_uuid) for asset in log.locations (asset.asset_type, asset.farmos_uuid) for asset in log.locations
@ -1066,6 +1077,32 @@ class LogImporterBase(FromFarmOS, ToWutta):
) )
log.assets.remove(asset) 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 "locations" in self.fields:
if not target_data or target_data["locations"] != source_data["locations"]: if not target_data or target_data["locations"] != source_data["locations"]:
@ -1126,18 +1163,6 @@ class ActivityLogImporter(LogImporterBase):
model_class = model.ActivityLog model_class = model.ActivityLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"is_group_assignment",
"notes",
"status",
"assets",
]
class HarvestLogImporter(LogImporterBase): class HarvestLogImporter(LogImporterBase):
""" """
@ -1146,18 +1171,6 @@ class HarvestLogImporter(LogImporterBase):
model_class = model.HarvestLog model_class = model.HarvestLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"is_group_assignment",
"notes",
"status",
"assets",
]
class MedicalLogImporter(LogImporterBase): class MedicalLogImporter(LogImporterBase):
""" """
@ -1166,18 +1179,6 @@ class MedicalLogImporter(LogImporterBase):
model_class = model.MedicalLog model_class = model.MedicalLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"is_group_assignment",
"notes",
"status",
"assets",
]
class ObservationLogImporter(LogImporterBase): class ObservationLogImporter(LogImporterBase):
""" """
@ -1186,18 +1187,6 @@ class ObservationLogImporter(LogImporterBase):
model_class = model.ObservationLog model_class = model.ObservationLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"is_group_assignment",
"notes",
"status",
"assets",
]
class QuantityImporterBase(FromFarmOS, ToWutta): class QuantityImporterBase(FromFarmOS, ToWutta):
""" """

View file

@ -96,6 +96,8 @@ class Normalizer(GenericHandler):
log_type_object = {} log_type_object = {}
log_type_uuid = None log_type_uuid = None
asset_objects = [] asset_objects = []
group_objects = []
group_uuids = []
quantity_objects = [] quantity_objects = []
quantity_uuids = [] quantity_uuids = []
location_objects = [] location_objects = []
@ -134,6 +136,30 @@ class Normalizer(GenericHandler):
) )
asset_objects.append(asset_object) 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"): if locations := relationships.get("location"):
for location in locations["data"]: for location in locations["data"]:
location_uuid = location["id"] location_uuid = location["id"]
@ -214,6 +240,8 @@ class Normalizer(GenericHandler):
"name": log["attributes"]["name"], "name": log["attributes"]["name"],
"timestamp": timestamp, "timestamp": timestamp,
"assets": asset_objects, "assets": asset_objects,
"groups": group_objects,
"group_uuids": group_uuids,
"quantities": quantity_objects, "quantities": quantity_objects,
"quantity_uuids": quantity_uuids, "quantity_uuids": quantity_uuids,
"is_group_assignment": log["attributes"]["is_group_assignment"], "is_group_assignment": log["attributes"]["is_group_assignment"],

View file

@ -87,6 +87,7 @@ class LogMasterView(FarmOSMasterView):
"name", "name",
"timestamp", "timestamp",
"assets", "assets",
"groups",
"locations", "locations",
"quantities", "quantities",
"notes", "notes",
@ -99,7 +100,7 @@ class LogMasterView(FarmOSMasterView):
] ]
def get_farmos_api_includes(self): 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): def get_grid_data(self, **kwargs):
return ResourceData( return ResourceData(
@ -144,8 +145,11 @@ class LogMasterView(FarmOSMasterView):
# assets # assets
g.set_renderer("assets", self.render_assets_for_grid) g.set_renderer("assets", self.render_assets_for_grid)
# groups
g.set_renderer("groups", self.render_assets_for_grid)
# locations # locations
g.set_renderer("locations", self.render_locations_for_grid) g.set_renderer("locations", self.render_assets_for_grid)
# quantities # quantities
g.set_renderer("quantities", self.render_quantities_for_grid) 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) g.set_renderer("owners", self.render_owners_for_grid)
def render_assets_for_grid(self, log, field, value): def render_assets_for_grid(self, log, field, value):
if not value:
return ""
assets = [] assets = []
for asset in value: for asset in value:
if self.farmos_style_grid_links: if self.farmos_style_grid_links:
@ -171,23 +178,6 @@ class LogMasterView(FarmOSMasterView):
assets.append(asset["name"]) assets.append(asset["name"])
return ", ".join(assets) 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): def render_quantities_for_grid(self, log, field, value):
if not value: if not value:
return None return None
@ -235,6 +225,9 @@ class LogMasterView(FarmOSMasterView):
# assets # assets
f.set_node("assets", FarmOSAssetRefs(self.request)) f.set_node("assets", FarmOSAssetRefs(self.request))
# groups
f.set_node("groups", FarmOSAssetRefs(self.request))
# locations # locations
f.set_node("locations", FarmOSAssetRefs(self.request)) f.set_node("locations", FarmOSAssetRefs(self.request))

View file

@ -125,6 +125,7 @@ class LogMasterView(WuttaFarmMasterView):
"message", "message",
"timestamp", "timestamp",
"assets", "assets",
"groups",
"locations", "locations",
"quantity", "quantity",
"notes", "notes",
@ -182,6 +183,9 @@ class LogMasterView(WuttaFarmMasterView):
# assets # assets
g.set_renderer("assets", self.render_assets_for_grid) g.set_renderer("assets", self.render_assets_for_grid)
# groups
g.set_renderer("groups", self.render_assets_for_grid)
# locations # locations
g.set_renderer("locations", self.render_assets_for_grid) 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 # nb. must explicity declare value for non-standard field
f.set_default("assets", log.assets) 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 # locations
if self.creating or self.editing: if self.creating or self.editing:
f.remove("locations") # TODO: need to support this f.remove("locations") # TODO: need to support this