fix: add parent relationships support for land assets

this may not be complete yet, we'll see.  works for the simple case afaik
This commit is contained in:
Lance Edgar 2026-02-14 22:50:34 -06:00
parent 71592e883a
commit ac084c4e79
7 changed files with 294 additions and 4 deletions

View file

@ -0,0 +1,125 @@
"""add LandAssetParent model
Revision ID: 554e6168c339
Revises: 8cc1565d38e7
Create Date: 2026-02-14 20:41:24.859064
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "554e6168c339"
down_revision: Union[str, None] = "8cc1565d38e7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# land_asset_parent
op.create_table(
"land_asset_parent",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("land_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("parent_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["land_asset_uuid"],
["land_asset.uuid"],
name=op.f("fk_land_asset_parent_land_asset_uuid_land_asset"),
),
sa.ForeignKeyConstraint(
["parent_asset_uuid"],
["land_asset.uuid"],
name=op.f("fk_land_asset_parent_parent_asset_uuid_land_asset"),
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset_parent")),
)
op.create_table(
"land_asset_parent_version",
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
sa.Column(
"land_asset_uuid",
wuttjamaican.db.util.UUID(),
autoincrement=False,
nullable=True,
),
sa.Column(
"parent_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_land_asset_parent_version")
),
)
op.create_index(
op.f("ix_land_asset_parent_version_end_transaction_id"),
"land_asset_parent_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_land_asset_parent_version_operation_type"),
"land_asset_parent_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_land_asset_parent_version_pk_transaction_id",
"land_asset_parent_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_land_asset_parent_version_pk_validity",
"land_asset_parent_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_land_asset_parent_version_transaction_id"),
"land_asset_parent_version",
["transaction_id"],
unique=False,
)
def downgrade() -> None:
# land_asset_parent
op.drop_index(
op.f("ix_land_asset_parent_version_transaction_id"),
table_name="land_asset_parent_version",
)
op.drop_index(
"ix_land_asset_parent_version_pk_validity",
table_name="land_asset_parent_version",
)
op.drop_index(
"ix_land_asset_parent_version_pk_transaction_id",
table_name="land_asset_parent_version",
)
op.drop_index(
op.f("ix_land_asset_parent_version_operation_type"),
table_name="land_asset_parent_version",
)
op.drop_index(
op.f("ix_land_asset_parent_version_end_transaction_id"),
table_name="land_asset_parent_version",
)
op.drop_table("land_asset_parent_version")
op.drop_table("land_asset_parent")

View file

@ -31,7 +31,7 @@ from .users import WuttaFarmUser
# wuttafarm proper models
from .assets import AssetType
from .land import LandType, LandAsset
from .land import LandType, LandAsset, LandAssetParent
from .structures import StructureType, Structure
from .animals import AnimalType, Animal
from .groups import Group

View file

@ -153,5 +153,39 @@ class LandAsset(model.Base):
""",
)
_parents = orm.relationship(
"LandAssetParent",
foreign_keys="LandAssetParent.land_asset_uuid",
back_populates="land_asset",
cascade="all, delete-orphan",
cascade_backrefs=False,
)
def __str__(self):
return self.name or ""
class LandAssetParent(model.Base):
"""
Represents a "land asset's parent relationship" from farmOS.
"""
__tablename__ = "land_asset_parent"
__versioned__ = {}
uuid = model.uuid_column()
land_asset_uuid = model.uuid_fk_column("land_asset.uuid", nullable=False)
land_asset = orm.relationship(
LandAsset,
foreign_keys=land_asset_uuid,
back_populates="_parents",
)
parent_asset_uuid = model.uuid_fk_column("land_asset.uuid", nullable=False)
parent_asset = orm.relationship(
LandAsset,
foreign_keys=parent_asset_uuid,
)

View file

@ -393,9 +393,11 @@ class LandAssetImporter(FromFarmOS, ToWutta):
"is_fixed",
"notes",
"archived",
"parents",
]
def setup(self):
""" """
super().setup()
model = self.app.model
@ -408,6 +410,17 @@ class LandAssetImporter(FromFarmOS, ToWutta):
land_assets = self.farmos_client.asset.get("land")
return land_assets["data"]
def normalize_source_data(self, **kwargs):
""" """
data = super().normalize_source_data(**kwargs)
if "parents" in self.fields:
# nb. make sure parent-less (root) assets come first, so they
# exist when child assets need to reference them
data.sort(key=lambda l: len(l["parents"]))
return data
def normalize_source_object(self, land):
""" """
land_type_id = land["attributes"]["land_type"]
@ -426,7 +439,7 @@ class LandAssetImporter(FromFarmOS, ToWutta):
else:
archived = land["attributes"]["status"] == "archived"
return {
data = {
"farmos_uuid": UUID(land["id"]),
"drupal_id": land["attributes"]["drupal_internal__id"],
"name": land["attributes"]["name"],
@ -437,6 +450,57 @@ class LandAssetImporter(FromFarmOS, ToWutta):
"notes": notes,
}
if "parents" in self.fields:
data["parents"] = []
for parent in land["relationships"]["parent"]["data"]:
assert parent["type"] == "asset--land"
data["parents"].append(UUID(parent["id"]))
return data
def normalize_target_object(self, land):
data = super().normalize_target_object(land)
if "parents" in self.fields:
data["parents"] = [p.parent_asset.farmos_uuid for p in land._parents]
return data
def update_target_object(self, land, source_data, target_data=None):
model = self.app.model
land = super().update_target_object(land, source_data, target_data)
if "parents" in self.fields:
if not target_data or target_data["parents"] != source_data["parents"]:
for farmos_uuid in source_data["parents"]:
if not target_data or farmos_uuid not in target_data["parents"]:
self.target_session.flush()
parent = (
self.target_session.query(model.LandAsset)
.filter(model.LandAsset.farmos_uuid == farmos_uuid)
.one()
)
land._parents.append(model.LandAssetParent(parent_asset=parent))
if target_data:
for farmos_uuid in target_data["parents"]:
if farmos_uuid not in source_data["parents"]:
parent = (
self.target_session.query(model.LandAsset)
.filter(model.LandAsset.farmos_uuid == farmos_uuid)
.one()
)
parent = (
self.target_session.query(model.LandAssetParent)
.filter(model.LandAssetParent.land_asset == land)
.filter(model.LandAssetParent.parent_asset == parent)
.one()
)
self.target_session.delete(parent)
return land
class LandTypeImporter(FromFarmOS, ToWutta):
"""

View file

@ -27,7 +27,7 @@ import json
import colander
from wuttaweb.forms.schema import ObjectRef
from wuttaweb.forms.schema import ObjectRef, WuttaSet
class AnimalTypeRef(ObjectRef):
@ -160,3 +160,20 @@ class UsersType(colander.SchemaType):
from wuttafarm.web.forms.widgets import UsersWidget
return UsersWidget(self.request, **kwargs)
class LandParentRefs(WuttaSet):
"""
Schema type for Parents field which references land assets.
"""
def serialize(self, node, appstruct):
if not appstruct:
appstruct = []
uuids = [u.hex for u in appstruct]
return json.dumps(uuids)
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import LandParentRefsWidget
return LandParentRefsWidget(self.request, **kwargs)

View file

@ -29,6 +29,9 @@ import colander
from deform.widget import Widget
from webhelpers2.html import HTML, tags
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
from wuttaweb.db import Session
class ImageWidget(Widget):
"""
@ -132,3 +135,34 @@ class UsersWidget(Widget):
return HTML.tag("ul", c=items)
return super().serialize(field, cstruct, **kw)
class LandParentRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for Parents field which references land assets.
"""
def serialize(self, field, cstruct, **kw):
""" """
model = self.app.model
session = Session()
readonly = kw.get("readonly", self.readonly)
if readonly:
parents = []
for uuid in json.loads(cstruct):
parent = session.get(model.LandAsset, uuid)
parents.append(
HTML.tag(
"li",
c=tags.link_to(
str(parent),
self.request.route_url(
"land_assets.view", uuid=parent.uuid
),
),
)
)
return HTML.tag("ul", c=parents)
return super().serialize(field, cstruct, **kw)

View file

@ -23,9 +23,11 @@
Master view for Land Assets
"""
from webhelpers2.html import HTML, tags
from wuttafarm.db.model.land import LandAsset
from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.web.forms.schema import LandTypeRef
from wuttafarm.web.forms.schema import LandTypeRef, LandParentRefs
class LandAssetView(WuttaFarmMasterView):
@ -47,6 +49,7 @@ class LandAssetView(WuttaFarmMasterView):
"drupal_id",
"name",
"land_type",
"parents",
"archived",
]
@ -59,6 +62,7 @@ class LandAssetView(WuttaFarmMasterView):
form_fields = [
"name",
"parents",
"notes",
"asset_type",
"land_type",
@ -85,6 +89,13 @@ class LandAssetView(WuttaFarmMasterView):
g.set_sorter("land_type", model.LandType.name)
g.set_filter("land_type", model.LandType.name, label="Land Type Name")
# parents
g.set_renderer("parents", self.render_parents_for_grid)
def render_parents_for_grid(self, land, field, value):
parents = [str(p.parent_asset) for p in land._parents]
return ", ".join(parents)
def grid_row_class(self, land, data, i):
""" """
if land.archived:
@ -94,6 +105,11 @@ class LandAssetView(WuttaFarmMasterView):
def configure_form(self, form):
f = form
super().configure_form(f)
land = f.model_instance
# parents
f.set_node("parents", LandParentRefs(self.request))
f.set_default("parents", [p.parent_asset_uuid for p in land._parents])
# notes
f.set_widget("notes", "notes")