From 7b6280b6dc24d2c919645f851d9bbd0ff51c703c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Feb 2026 12:14:35 -0600 Subject: [PATCH] feat: convert land assets to use common base/mixin --- ...82c82f9_use_shared_base_for_land_assets.py | 411 ++++++++++++++++++ src/wuttafarm/db/model/__init__.py | 4 +- src/wuttafarm/db/model/assets.py | 33 ++ src/wuttafarm/db/model/land.py | 105 +---- src/wuttafarm/importing/farmos.py | 176 ++++---- src/wuttafarm/web/forms/schema.py | 8 +- src/wuttafarm/web/forms/widgets.py | 8 +- src/wuttafarm/web/menus.py | 20 +- src/wuttafarm/web/views/__init__.py | 3 +- src/wuttafarm/web/views/assets.py | 50 ++- .../web/views/{land_types.py => land.py} | 91 +++- src/wuttafarm/web/views/land_assets.py | 156 ------- 12 files changed, 691 insertions(+), 374 deletions(-) create mode 100644 src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py rename src/wuttafarm/web/views/{land_types.py => land.py} (55%) delete mode 100644 src/wuttafarm/web/views/land_assets.py diff --git a/src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py b/src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py new file mode 100644 index 0000000..7c9b5f7 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py @@ -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") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index a549879..94592f2 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -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 diff --git a/src/wuttafarm/db/model/assets.py b/src/wuttafarm/db/model/assets.py index 85c7eb4..531fd62 100644 --- a/src/wuttafarm/db/model/assets.py +++ b/src/wuttafarm/db/model/assets.py @@ -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, + ) diff --git a/src/wuttafarm/db/model/land.py b/src/wuttafarm/db/model/land.py index da94cf1..1221c63 100644 --- a/src/wuttafarm/db/model/land.py +++ b/src/wuttafarm/db/model/land.py @@ -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) diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 6d8c573..fadb43e 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -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"], - "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"])) - + data = self.normalize_asset(land) + data.update( + { + "asset_type": "land", + "land_type_uuid": land_type.uuid, + } + ) 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): """ diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py index 7c048ee..95b3e9d 100644 --- a/src/wuttafarm/web/forms/schema.py +++ b/src/wuttafarm/web/forms/schema.py @@ -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) diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py index 45519b9..f812ccf 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -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 ), ), ) diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 071e5b6..9625af0 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -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", diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index d27e6d9..3963014 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -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") diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py index b8b1dfc..81fd093 100644 --- a/src/wuttafarm/web/views/assets.py +++ b/src/wuttafarm/web/views/assets.py @@ -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( diff --git a/src/wuttafarm/web/views/land_types.py b/src/wuttafarm/web/views/land.py similarity index 55% rename from src/wuttafarm/web/views/land_types.py rename to src/wuttafarm/web/views/land.py index 20d8a21..ce577c9 100644 --- a/src/wuttafarm/web/views/land_types.py +++ b/src/wuttafarm/web/views/land.py @@ -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) diff --git a/src/wuttafarm/web/views/land_assets.py b/src/wuttafarm/web/views/land_assets.py deleted file mode 100644 index 7105465..0000000 --- a/src/wuttafarm/web/views/land_assets.py +++ /dev/null @@ -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 . -# -################################################################################ -""" -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)