feat: convert land assets to use common base/mixin

This commit is contained in:
Lance Edgar 2026-02-15 12:14:35 -06:00
parent 140f3cbdba
commit 7b6280b6dc
12 changed files with 691 additions and 374 deletions

View file

@ -0,0 +1,411 @@
"""use shared base for Land Assets
Revision ID: d882682c82f9
Revises: d6e8d16d6854
Create Date: 2026-02-15 12:00:27.036011
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "d882682c82f9"
down_revision: Union[str, None] = "d6e8d16d6854"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# asset_parent
op.create_table(
"asset_parent",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("parent_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["asset_uuid"],
["asset.uuid"],
name=op.f("fk_asset_parent_asset_uuid_asset"),
),
sa.ForeignKeyConstraint(
["parent_uuid"],
["asset.uuid"],
name=op.f("fk_asset_parent_parent_uuid_asset"),
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_parent")),
)
op.create_table(
"asset_parent_version",
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
sa.Column(
"asset_uuid",
wuttjamaican.db.util.UUID(),
autoincrement=False,
nullable=True,
),
sa.Column(
"parent_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_asset_parent_version")
),
)
op.create_index(
op.f("ix_asset_parent_version_end_transaction_id"),
"asset_parent_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_asset_parent_version_operation_type"),
"asset_parent_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_asset_parent_version_pk_transaction_id",
"asset_parent_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_asset_parent_version_pk_validity",
"asset_parent_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_asset_parent_version_transaction_id"),
"asset_parent_version",
["transaction_id"],
unique=False,
)
# asset_land
op.create_table(
"asset_land",
sa.Column("land_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["land_type_uuid"],
["land_type.uuid"],
name=op.f("fk_asset_land_land_type_uuid_land_type"),
),
sa.ForeignKeyConstraint(
["uuid"], ["asset.uuid"], name=op.f("fk_asset_land_uuid_asset")
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_land")),
sa.UniqueConstraint(
"land_type_uuid", name=op.f("uq_asset_land_land_type_uuid")
),
)
op.create_table(
"asset_land_version",
sa.Column(
"land_type_uuid",
wuttjamaican.db.util.UUID(),
autoincrement=False,
nullable=True,
),
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
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_asset_land_version")
),
)
op.create_index(
op.f("ix_asset_land_version_end_transaction_id"),
"asset_land_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_asset_land_version_operation_type"),
"asset_land_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_asset_land_version_pk_transaction_id",
"asset_land_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_asset_land_version_pk_validity",
"asset_land_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_asset_land_version_transaction_id"),
"asset_land_version",
["transaction_id"],
unique=False,
)
# land_asset_parent
op.drop_index(
op.f("ix_land_asset_parent_version_end_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_pk_transaction_id"),
table_name="land_asset_parent_version",
)
op.drop_index(
op.f("ix_land_asset_parent_version_pk_validity"),
table_name="land_asset_parent_version",
)
op.drop_index(
op.f("ix_land_asset_parent_version_transaction_id"),
table_name="land_asset_parent_version",
)
op.drop_table("land_asset_parent_version")
op.drop_table("land_asset_parent")
# land_asset
op.drop_index(
op.f("ix_land_asset_version_end_transaction_id"),
table_name="land_asset_version",
)
op.drop_index(
op.f("ix_land_asset_version_operation_type"), table_name="land_asset_version"
)
op.drop_index(
op.f("ix_land_asset_version_pk_transaction_id"), table_name="land_asset_version"
)
op.drop_index(
op.f("ix_land_asset_version_pk_validity"), table_name="land_asset_version"
)
op.drop_index(
op.f("ix_land_asset_version_transaction_id"), table_name="land_asset_version"
)
op.drop_table("land_asset_version")
op.drop_table("land_asset")
def downgrade() -> None:
# land_asset
op.create_table(
"land_asset",
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column("land_type_uuid", sa.UUID(), autoincrement=False, nullable=False),
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(
["land_type_uuid"],
["land_type.uuid"],
name=op.f("fk_land_asset_land_type_uuid_land_type"),
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset")),
sa.UniqueConstraint(
"drupal_id",
name=op.f("uq_land_asset_drupal_id"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
sa.UniqueConstraint(
"farmos_uuid",
name=op.f("uq_land_asset_farmos_uuid"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
sa.UniqueConstraint(
"land_type_uuid",
name=op.f("uq_land_asset_land_type_uuid"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
sa.UniqueConstraint(
"name",
name=op.f("uq_land_asset_name"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
)
op.create_table(
"land_asset_version",
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
sa.Column("land_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
sa.Column(
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
),
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint(
"uuid", "transaction_id", name=op.f("pk_land_asset_version")
),
)
op.create_index(
op.f("ix_land_asset_version_transaction_id"),
"land_asset_version",
["transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_land_asset_version_pk_validity"),
"land_asset_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_land_asset_version_pk_transaction_id"),
"land_asset_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
op.f("ix_land_asset_version_operation_type"),
"land_asset_version",
["operation_type"],
unique=False,
)
op.create_index(
op.f("ix_land_asset_version_end_transaction_id"),
"land_asset_version",
["end_transaction_id"],
unique=False,
)
# land_asset_parent
op.create_table(
"land_asset_parent",
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
sa.Column("land_asset_uuid", sa.UUID(), autoincrement=False, nullable=False),
sa.Column("parent_asset_uuid", sa.UUID(), autoincrement=False, 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", sa.UUID(), autoincrement=False, nullable=False),
sa.Column("land_asset_uuid", sa.UUID(), autoincrement=False, nullable=True),
sa.Column("parent_asset_uuid", sa.UUID(), autoincrement=False, nullable=True),
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
sa.Column(
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
),
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, 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_transaction_id"),
"land_asset_parent_version",
["transaction_id"],
unique=False,
)
op.create_index(
op.f("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_pk_transaction_id"),
"land_asset_parent_version",
["uuid", sa.literal_column("transaction_id DESC")],
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(
op.f("ix_land_asset_parent_version_end_transaction_id"),
"land_asset_parent_version",
["end_transaction_id"],
unique=False,
)
# asset_land
op.drop_table("asset_land")
op.drop_index(
op.f("ix_asset_land_version_transaction_id"), table_name="asset_land_version"
)
op.drop_index("ix_asset_land_version_pk_validity", table_name="asset_land_version")
op.drop_index(
"ix_asset_land_version_pk_transaction_id", table_name="asset_land_version"
)
op.drop_index(
op.f("ix_asset_land_version_operation_type"), table_name="asset_land_version"
)
op.drop_index(
op.f("ix_asset_land_version_end_transaction_id"),
table_name="asset_land_version",
)
op.drop_table("asset_land_version")
# asset_parent
op.drop_index(
op.f("ix_asset_parent_version_transaction_id"),
table_name="asset_parent_version",
)
op.drop_index(
"ix_asset_parent_version_pk_validity", table_name="asset_parent_version"
)
op.drop_index(
"ix_asset_parent_version_pk_transaction_id", table_name="asset_parent_version"
)
op.drop_index(
op.f("ix_asset_parent_version_operation_type"),
table_name="asset_parent_version",
)
op.drop_index(
op.f("ix_asset_parent_version_end_transaction_id"),
table_name="asset_parent_version",
)
op.drop_table("asset_parent_version")
op.drop_table("asset_parent")

View file

@ -30,8 +30,8 @@ from wuttjamaican.db.model import *
from .users import WuttaFarmUser
# wuttafarm proper models
from .assets import AssetType, Asset
from .land import LandType, LandAsset, LandAssetParent
from .assets import AssetType, Asset, AssetParent
from .land import LandType, LandAsset
from .structures import StructureType, Structure
from .animals import AnimalType, AnimalAsset
from .groups import Group

View file

@ -178,6 +178,14 @@ class Asset(model.Base):
""",
)
_parents = orm.relationship(
"AssetParent",
foreign_keys="AssetParent.asset_uuid",
back_populates="asset",
cascade="all, delete-orphan",
cascade_backrefs=False,
)
def __str__(self):
return self.asset_name or ""
@ -205,3 +213,28 @@ def add_asset_proxies(subclass):
Asset.make_proxy(subclass, "asset", "thumbnail_url")
Asset.make_proxy(subclass, "asset", "image_url")
Asset.make_proxy(subclass, "asset", "archived")
class AssetParent(model.Base):
"""
Represents an "asset's parent relationship" from farmOS.
"""
__tablename__ = "asset_parent"
__versioned__ = {}
uuid = model.uuid_column()
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
asset = orm.relationship(
Asset,
foreign_keys=asset_uuid,
)
parent_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
parent = orm.relationship(
Asset,
foreign_keys=parent_uuid,
)

View file

@ -28,6 +28,8 @@ from sqlalchemy import orm
from wuttjamaican.db import model
from wuttafarm.db.model.assets import AssetMixin, add_asset_proxies
class LandType(model.Base):
"""
@ -76,116 +78,21 @@ class LandType(model.Base):
return self.name or ""
class LandAsset(model.Base):
class LandAsset(AssetMixin, model.Base):
"""
Represents a "land asset" from farmOS
"""
__tablename__ = "land_asset"
__tablename__ = "asset_land"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Land Asset",
"model_title_plural": "Land Assets",
"farmos_asset_type": "animal",
}
uuid = model.uuid_column()
name = sa.Column(
sa.String(length=100),
nullable=False,
unique=True,
doc="""
Name of the land asset.
""",
)
land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False, unique=True)
land_type = orm.relationship(LandType, back_populates="land_assets")
is_location = sa.Column(
sa.Boolean(),
nullable=False,
doc="""
Whether the land asset should be considered a location.
""",
)
is_fixed = sa.Column(
sa.Boolean(),
nullable=False,
doc="""
Whether the land asset's location is fixed.
""",
)
notes = sa.Column(
sa.Text(),
nullable=True,
doc="""
Notes for the land asset.
""",
)
archived = sa.Column(
sa.Boolean(),
nullable=False,
default=False,
doc="""
Whether the land asset is archived.
""",
)
farmos_uuid = sa.Column(
model.UUID(),
nullable=True,
unique=True,
doc="""
UUID for the land asset within farmOS.
""",
)
drupal_id = sa.Column(
sa.Integer(),
nullable=True,
unique=True,
doc="""
Drupal internal ID for the land asset.
""",
)
_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,
)
add_asset_proxies(LandAsset)

View file

@ -191,6 +191,8 @@ class AssetImporterBase(FromFarmOS, ToWutta):
"drupal_id",
"asset_type",
"asset_name",
"is_location",
"is_fixed",
"notes",
"archived",
"image_url",
@ -199,6 +201,27 @@ class AssetImporterBase(FromFarmOS, ToWutta):
)
return fields
def get_supported_fields(self):
""" """
fields = list(super().get_supported_fields())
fields.extend(
[
"parents",
]
)
return fields
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_asset(self, asset):
""" """
image_url = None
@ -224,16 +247,78 @@ class AssetImporterBase(FromFarmOS, ToWutta):
else:
archived = asset["attributes"]["status"] == "archived"
parents = None
if "parents" in self.fields:
parents = []
for parent in asset["relationships"]["parent"]["data"]:
parents.append((self.get_asset_type(parent), UUID(parent["id"])))
return {
"farmos_uuid": UUID(asset["id"]),
"drupal_id": asset["attributes"]["drupal_internal__id"],
"asset_name": asset["attributes"]["name"],
"is_location": asset["attributes"]["is_location"],
"is_fixed": asset["attributes"]["is_fixed"],
"archived": archived,
"notes": notes,
"image_url": image_url,
"thumbnail_url": thumbnail_url,
"parents": parents,
}
def get_asset_type(self, asset):
return asset["type"].split("--")[1]
def normalize_target_object(self, asset):
data = super().normalize_target_object(asset)
if "parents" in self.fields:
data["parents"] = [
(p.parent.asset_type, p.parent.farmos_uuid)
for p in asset.asset._parents
]
return data
def update_target_object(self, asset, source_data, target_data=None):
model = self.app.model
asset = super().update_target_object(asset, source_data, target_data)
if "parents" in self.fields:
if not target_data or target_data["parents"] != source_data["parents"]:
for key in source_data["parents"]:
asset_type, farmos_uuid = key
if not target_data or key not in target_data["parents"]:
self.target_session.flush()
parent = (
self.target_session.query(model.Asset)
.filter(model.Asset.asset_type == asset_type)
.filter(model.Asset.farmos_uuid == farmos_uuid)
.one()
)
asset.asset._parents.append(model.AssetParent(parent=parent))
if target_data:
for key in target_data["parents"]:
asset_type, farmos_uuid = key
if key not in source_data["parents"]:
parent = (
self.target_session.query(model.Asset)
.filter(model.Asset.asset_type == asset_type)
.filter(model.Asset.farmos_uuid == farmos_uuid)
.one()
)
parent = (
self.target_session.query(model.AssetParent)
.filter(model.AssetParent.asset == asset)
.filter(model.AssetParent.parent == parent)
.one()
)
self.target_session.delete(parent)
return asset
class AnimalAssetImporter(AssetImporterBase):
"""
@ -418,7 +503,7 @@ class GroupImporter(FromFarmOS, ToWutta):
}
class LandAssetImporter(FromFarmOS, ToWutta):
class LandAssetImporter(AssetImporterBase):
"""
farmOS API WuttaFarm importer for Land Assets
"""
@ -428,7 +513,8 @@ class LandAssetImporter(FromFarmOS, ToWutta):
supported_fields = [
"farmos_uuid",
"drupal_id",
"name",
"asset_type",
"asset_name",
"land_type_uuid",
"is_location",
"is_fixed",
@ -451,17 +537,6 @@ 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"]
@ -472,76 +547,15 @@ class LandAssetImporter(FromFarmOS, ToWutta):
)
return None
if notes := land["attributes"]["notes"]:
notes = notes["value"]
if self.farmos_4x:
archived = land["attributes"]["archived"]
else:
archived = land["attributes"]["status"] == "archived"
data = {
"farmos_uuid": UUID(land["id"]),
"drupal_id": land["attributes"]["drupal_internal__id"],
"name": land["attributes"]["name"],
data = self.normalize_asset(land)
data.update(
{
"asset_type": "land",
"land_type_uuid": land_type.uuid,
"is_location": land["attributes"]["is_location"],
"is_fixed": land["attributes"]["is_fixed"],
"archived": archived,
"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

@ -162,9 +162,9 @@ class UsersType(colander.SchemaType):
return UsersWidget(self.request, **kwargs)
class LandParentRefs(WuttaSet):
class AssetParentRefs(WuttaSet):
"""
Schema type for Parents field which references land assets.
Schema type for Parents field which references assets.
"""
def serialize(self, node, appstruct):
@ -174,6 +174,6 @@ class LandParentRefs(WuttaSet):
return json.dumps(uuids)
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import LandParentRefsWidget
from wuttafarm.web.forms.widgets import AssetParentRefsWidget
return LandParentRefsWidget(self.request, **kwargs)
return AssetParentRefsWidget(self.request, **kwargs)

View file

@ -137,9 +137,9 @@ class UsersWidget(Widget):
return super().serialize(field, cstruct, **kw)
class LandParentRefsWidget(WuttaCheckboxChoiceWidget):
class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for Parents field which references land assets.
Widget for Parents field which references assets.
"""
def serialize(self, field, cstruct, **kw):
@ -151,14 +151,14 @@ class LandParentRefsWidget(WuttaCheckboxChoiceWidget):
if readonly:
parents = []
for uuid in json.loads(cstruct):
parent = session.get(model.LandAsset, uuid)
parent = session.get(model.Asset, uuid)
parents.append(
HTML.tag(
"li",
c=tags.link_to(
str(parent),
self.request.route_url(
"land_assets.view", uuid=parent.uuid
f"{parent.asset_type}_assets.view", uuid=parent.uuid
),
),
)

View file

@ -54,6 +54,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "animal_assets",
"perm": "animal_assets.list",
},
{
"title": "Land",
"route": "land_assets",
"perm": "land_assets.list",
},
{"type": "sep"},
{
"title": "Groups",
@ -65,27 +70,22 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "structures",
"perm": "structures.list",
},
{
"title": "Land",
"route": "land_assets",
"perm": "land_assets.list",
},
{"type": "sep"},
{
"title": "Animal Types",
"route": "animal_types",
"perm": "animal_types.list",
},
{
"title": "Structure Types",
"route": "structure_types",
"perm": "structure_types.list",
},
{
"title": "Land Types",
"route": "land_types",
"perm": "land_types.list",
},
{
"title": "Structure Types",
"route": "structure_types",
"perm": "structure_types.list",
},
{
"title": "Asset Types",
"route": "asset_types",

View file

@ -43,9 +43,8 @@ def includeme(config):
# native table views
config.include("wuttafarm.web.views.asset_types")
config.include("wuttafarm.web.views.assets")
config.include("wuttafarm.web.views.land_types")
config.include("wuttafarm.web.views.land")
config.include("wuttafarm.web.views.structure_types")
config.include("wuttafarm.web.views.land_assets")
config.include("wuttafarm.web.views.structures")
config.include("wuttafarm.web.views.animals")
config.include("wuttafarm.web.views.groups")

View file

@ -25,11 +25,26 @@ Master view for Assets
from collections import OrderedDict
from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Asset
from wuttafarm.web.forms.schema import AssetParentRefs
from wuttafarm.web.forms.widgets import ImageWidget
def get_asset_type_enum(config):
app = config.get_app()
model = app.model
session = Session()
asset_types = OrderedDict()
query = session.query(model.AssetType).order_by(model.AssetType.name)
for asset_type in query:
asset_types[asset_type.drupal_id] = asset_type.name
return asset_types
class AssetView(WuttaFarmMasterView):
"""
Master view for Assets
@ -51,6 +66,7 @@ class AssetView(WuttaFarmMasterView):
"drupal_id",
"asset_name",
"asset_type",
"parents",
"archived",
]
@ -77,7 +93,10 @@ class AssetView(WuttaFarmMasterView):
g.set_link("asset_name")
# asset_type
g.set_enum("asset_type", self.get_asset_type_enum())
g.set_enum("asset_type", get_asset_type_enum(self.config))
# parents
g.set_renderer("parents", self.render_parents_for_grid)
# view action links to final asset record
def asset_url(asset, i):
@ -87,15 +106,9 @@ class AssetView(WuttaFarmMasterView):
g.add_action("view", icon="eye", url=asset_url)
def get_asset_type_enum(self):
model = self.app.model
session = self.Session()
asset_types = OrderedDict()
query = session.query(model.AssetType).order_by(model.AssetType.name)
for asset_type in query:
asset_types[asset_type.drupal_id] = asset_type.name
return asset_types
def render_parents_for_grid(self, asset, field, value):
parents = [str(p.parent) for p in asset._parents]
return ", ".join(parents)
def grid_row_class(self, asset, data, i):
""" """
@ -143,11 +156,18 @@ class AssetMasterView(WuttaFarmMasterView):
g.set_sorter("asset_name", model.Asset.asset_name)
g.set_filter("asset_name", model.Asset.asset_name)
# parents
g.set_renderer("parents", self.render_parents_for_grid)
# archived
g.set_renderer("archived", "boolean")
g.set_sorter("archived", model.Asset.archived)
g.set_filter("archived", model.Asset.archived)
def render_parents_for_grid(self, asset, field, value):
parents = [str(p.parent) for p in asset.asset._parents]
return ", ".join(parents)
def grid_row_class(self, asset, data, i):
""" """
if asset.archived:
@ -163,8 +183,16 @@ class AssetMasterView(WuttaFarmMasterView):
if self.creating:
f.remove("asset_type")
else:
f.set_node(
"asset_type",
WuttaDictEnum(self.request, get_asset_type_enum(self.config)),
)
f.set_readonly("asset_type")
# parents
f.set_node("parents", AssetParentRefs(self.request))
f.set_default("parents", [p.parent_uuid for p in asset.asset._parents])
# notes
f.set_widget("notes", "notes")
@ -211,6 +239,8 @@ class AssetMasterView(WuttaFarmMasterView):
route = None
if asset.asset_type == "animal":
route = "farmos_animals.view"
elif asset.asset_type == "land":
route = "farmos_land_assets.view"
if route:
buttons.append(

View file

@ -23,8 +23,12 @@
Master view for Land Types
"""
from webhelpers2.html import HTML, tags
from wuttafarm.db.model.land import LandType, LandAsset
from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.web.views.assets import AssetMasterView
from wuttafarm.web.forms.schema import LandTypeRef
class LandTypeView(WuttaFarmMasterView):
@ -57,13 +61,13 @@ class LandTypeView(WuttaFarmMasterView):
rows_viewable = True
row_grid_columns = [
"name",
"asset_name",
"is_location",
"is_fixed",
"archived",
]
rows_sort_defaults = "name"
rows_sort_defaults = "asset_name"
def configure_grid(self, grid):
g = grid
@ -92,27 +96,102 @@ class LandTypeView(WuttaFarmMasterView):
def get_row_grid_data(self, land_type):
model = self.app.model
session = self.Session()
return session.query(model.LandAsset).filter(
model.LandAsset.land_type == land_type
return (
session.query(model.LandAsset)
.join(model.Asset)
.filter(model.LandAsset.land_type == land_type)
)
def configure_row_grid(self, grid):
g = grid
super().configure_row_grid(g)
model = self.app.model
# name
g.set_link("name")
# asset_name
g.set_link("asset_name")
g.set_sorter("asset_name", model.Asset.asset_name)
g.set_filter("asset_name", model.Asset.asset_name)
# is_location
g.set_renderer("is_location", "boolean")
g.set_sorter("is_location", model.Asset.is_location)
g.set_filter("is_location", model.Asset.is_location)
# is_fixed
g.set_renderer("is_fixed", "boolean")
g.set_sorter("is_fixed", model.Asset.is_fixed)
g.set_filter("is_fixed", model.Asset.is_fixed)
# archived
g.set_renderer("archived", "boolean")
g.set_sorter("archived", model.Asset.archived)
g.set_filter("archived", model.Asset.archived)
def get_row_action_url_view(self, land_asset, i):
return self.request.route_url("land_assets.view", uuid=land_asset.uuid)
class LandAssetView(AssetMasterView):
"""
Master view for Land Assets
"""
model_class = LandAsset
route_prefix = "land_assets"
url_prefix = "/assets/land"
farmos_refurl_path = "/assets/land"
grid_columns = [
"thumbnail",
"drupal_id",
"asset_name",
"land_type",
"parents",
"archived",
]
form_fields = [
"asset_name",
"parents",
"notes",
"asset_type",
"land_type",
"is_location",
"is_fixed",
"archived",
"farmos_uuid",
"drupal_id",
]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
model = self.app.model
# land_type
g.set_joiner("land_type", lambda q: q.join(model.LandType))
g.set_sorter("land_type", model.LandType.name)
g.set_filter("land_type", model.LandType.name, label="Land Type Name")
def configure_form(self, form):
f = form
super().configure_form(f)
land = f.model_instance
# land_type
f.set_node("land_type", LandTypeRef(self.request))
def defaults(config, **kwargs):
base = globals()
LandTypeView = kwargs.get("LandTypeView", base["LandTypeView"])
LandTypeView.defaults(config)
LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"])
LandAssetView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -1,156 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttaFarm --Web app to integrate with and extend farmOS
# Copyright © 2026 Lance Edgar
#
# This file is part of WuttaFarm.
#
# WuttaFarm is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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, LandParentRefs
class LandAssetView(WuttaFarmMasterView):
"""
Master view for Land Assets
"""
model_class = LandAsset
route_prefix = "land_assets"
url_prefix = "/land-assets"
farmos_refurl_path = "/assets/land"
labels = {
"name": "Asset Name",
}
grid_columns = [
"drupal_id",
"name",
"land_type",
"parents",
"archived",
]
sort_defaults = "name"
filter_defaults = {
"name": {"active": True, "verb": "contains"},
"archived": {"active": True, "verb": "is_false"},
}
form_fields = [
"name",
"parents",
"notes",
"asset_type",
"land_type",
"is_location",
"is_fixed",
"archived",
"farmos_uuid",
"drupal_id",
]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
model = self.app.model
# drupal_id
g.set_label("drupal_id", "ID", column_only=True)
# name
g.set_link("name")
# land_type
g.set_joiner("land_type", lambda q: q.join(model.LandType))
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:
return "has-background-warning"
return None
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")
# asset_type
if self.creating:
f.remove("asset_type")
else:
f.set_default("asset_type", "Land")
f.set_readonly("asset_type")
# land_type
f.set_node("land_type", LandTypeRef(self.request))
def get_farmos_url(self, land):
return self.app.get_farmos_url(f"/asset/{land.drupal_id}")
def get_xref_buttons(self, land_asset):
buttons = super().get_xref_buttons(land_asset)
if land_asset.farmos_uuid:
buttons.append(
self.make_button(
"View farmOS record",
primary=True,
url=self.request.route_url(
"farmos_land_assets.view", uuid=land_asset.farmos_uuid
),
icon_left="eye",
)
)
return buttons
def defaults(config, **kwargs):
base = globals()
LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"])
LandAssetView.defaults(config)
def includeme(config):
defaults(config)