diff --git a/src/wuttafarm/db/alembic/versions/de1197d24485_add_waterasset.py b/src/wuttafarm/db/alembic/versions/de1197d24485_add_waterasset.py new file mode 100644 index 0000000..e123fc3 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/de1197d24485_add_waterasset.py @@ -0,0 +1,100 @@ +"""add WaterAsset + +Revision ID: de1197d24485 +Revises: 9c53513f8862 +Create Date: 2026-03-09 14:59:12.032318 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "de1197d24485" +down_revision: Union[str, None] = "9c53513f8862" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # asset_water + op.create_table( + "asset_water", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.ForeignKeyConstraint( + ["uuid"], ["asset.uuid"], name=op.f("fk_asset_water_uuid_asset") + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_water")), + ) + op.create_table( + "asset_water_version", + 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_water_version") + ), + ) + op.create_index( + op.f("ix_asset_water_version_end_transaction_id"), + "asset_water_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_asset_water_version_operation_type"), + "asset_water_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_asset_water_version_pk_transaction_id", + "asset_water_version", + ["uuid", sa.literal_column("transaction_id DESC")], + unique=False, + ) + op.create_index( + "ix_asset_water_version_pk_validity", + "asset_water_version", + ["uuid", "transaction_id", "end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_asset_water_version_transaction_id"), + "asset_water_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # asset_water + op.drop_index( + op.f("ix_asset_water_version_transaction_id"), table_name="asset_water_version" + ) + op.drop_index( + "ix_asset_water_version_pk_validity", table_name="asset_water_version" + ) + op.drop_index( + "ix_asset_water_version_pk_transaction_id", table_name="asset_water_version" + ) + op.drop_index( + op.f("ix_asset_water_version_operation_type"), table_name="asset_water_version" + ) + op.drop_index( + op.f("ix_asset_water_version_end_transaction_id"), + table_name="asset_water_version", + ) + op.drop_table("asset_water_version") + op.drop_table("asset_water") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index 35130da..e2b96c4 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -51,6 +51,7 @@ from .asset_plant import ( PlantAssetPlantType, PlantAssetSeason, ) +from .asset_water import WaterAsset from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner from .log_activity import ActivityLog from .log_harvest import HarvestLog diff --git a/src/wuttafarm/db/model/asset_water.py b/src/wuttafarm/db/model/asset_water.py new file mode 100644 index 0000000..046c899 --- /dev/null +++ b/src/wuttafarm/db/model/asset_water.py @@ -0,0 +1,45 @@ +# -*- 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 . +# +################################################################################ +""" +Model definition for Water Assets +""" + +from wuttjamaican.db import model + +from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies + + +class WaterAsset(AssetMixin, model.Base): + """ + Represents a water asset from farmOS + """ + + __tablename__ = "asset_water" + __versioned__ = {} + __wutta_hint__ = { + "model_title": "Water Asset", + "model_title_plural": "Water Assets", + "farmos_asset_type": "water", + } + + +add_asset_proxies(WaterAsset) diff --git a/src/wuttafarm/farmos/importing/model.py b/src/wuttafarm/farmos/importing/model.py index 4905a5e..5e6f01b 100644 --- a/src/wuttafarm/farmos/importing/model.py +++ b/src/wuttafarm/farmos/importing/model.py @@ -491,6 +491,21 @@ class StructureAssetImporter(ToFarmOSAsset): return payload +class WaterAssetImporter(ToFarmOSAsset): + + model_title = "WaterAsset" + farmos_asset_type = "water" + + supported_fields = [ + "uuid", + "asset_name", + "is_location", + "is_fixed", + "notes", + "archived", + ] + + ############################## # quantity importers ############################## diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py index 7bb9538..7f99b51 100644 --- a/src/wuttafarm/farmos/importing/wuttafarm.py +++ b/src/wuttafarm/farmos/importing/wuttafarm.py @@ -98,6 +98,7 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler): importers = super().define_importers() importers["LandAsset"] = LandAssetImporter importers["StructureAsset"] = StructureAssetImporter + importers["WaterAsset"] = WaterAssetImporter importers["AnimalType"] = AnimalTypeImporter importers["AnimalAsset"] = AnimalAssetImporter importers["GroupAsset"] = GroupAssetImporter @@ -417,6 +418,14 @@ class StructureAssetImporter( return data +class WaterAssetImporter(FromWuttaFarmAsset, farmos_importing.model.WaterAssetImporter): + """ + WuttaFarm → farmOS API exporter for Water Assets + """ + + source_model_class = model.WaterAsset + + ############################## # quantity importers ############################## diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 695d373..a16393d 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -106,6 +106,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): importers["LandAsset"] = LandAssetImporter importers["StructureType"] = StructureTypeImporter importers["StructureAsset"] = StructureAssetImporter + importers["WaterAsset"] = WaterAssetImporter importers["AnimalType"] = AnimalTypeImporter importers["AnimalAsset"] = AnimalAssetImporter importers["GroupAsset"] = GroupAssetImporter @@ -830,6 +831,14 @@ class StructureTypeImporter(FromFarmOS, ToWutta): } +class WaterAssetImporter(AssetImporterBase): + """ + farmOS API → WuttaFarm importer for Water Assets + """ + + model_class = model.WaterAsset + + class UserImporter(FromFarmOS, ToWutta): """ farmOS API → WuttaFarm importer for Users diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 249c210..e518361 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -112,6 +112,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "structure_assets", "perm": "structure_assets.list", }, + { + "title": "Water", + "route": "water_assets", + "perm": "water_assets.list", + }, {"type": "sep"}, { "title": "Animal Types", @@ -259,6 +264,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_structure_assets", "perm": "farmos_structure_assets.list", }, + { + "title": "Water Assets", + "route": "farmos_water_assets", + "perm": "farmos_water_assets.list", + }, {"type": "sep"}, { "title": "Activity Logs", @@ -383,6 +393,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_structure_assets", "perm": "farmos_structure_assets.list", }, + { + "title": "Water", + "route": "farmos_water_assets", + "perm": "farmos_water_assets.list", + }, {"type": "sep"}, { "title": "Animal Types", diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index d189e42..94f8772 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -57,6 +57,7 @@ def includeme(config): config.include("wuttafarm.web.views.animals") config.include("wuttafarm.web.views.groups") config.include("wuttafarm.web.views.plants") + config.include("wuttafarm.web.views.water") config.include("wuttafarm.web.views.logs") config.include("wuttafarm.web.views.logs_activity") config.include("wuttafarm.web.views.logs_harvest") diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index 9cb57d5..2de8ff9 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -40,6 +40,7 @@ def includeme(config): config.include("wuttafarm.web.views.farmos.animals") config.include("wuttafarm.web.views.farmos.groups") config.include("wuttafarm.web.views.farmos.plants") + config.include("wuttafarm.web.views.farmos.water") config.include("wuttafarm.web.views.farmos.log_types") config.include("wuttafarm.web.views.farmos.logs_activity") config.include("wuttafarm.web.views.farmos.logs_harvest") diff --git a/src/wuttafarm/web/views/farmos/assets.py b/src/wuttafarm/web/views/farmos/assets.py index 11f744b..863b947 100644 --- a/src/wuttafarm/web/views/farmos/assets.py +++ b/src/wuttafarm/web/views/farmos/assets.py @@ -24,6 +24,7 @@ Base class for Asset master views """ import colander +import requests from webhelpers2.html import tags from wuttafarm.web.views.farmos import FarmOSMasterView @@ -75,6 +76,22 @@ class AssetMasterView(FarmOSMasterView): "archived": {"active": True, "verb": "is_false"}, } + form_fields = [ + "name", + "notes", + "asset_type_name", + "is_location", + "is_fixed", + "owners", + "locations", + "groups", + "archived", + "thumbnail_url", + "image_url", + "thumbnail", + "image", + ] + def get_grid_data(self, **kwargs): return ResourceData( self.config, @@ -142,11 +159,16 @@ class AssetMasterView(FarmOSMasterView): return {"asset_type", "location", "owner", "image"} def get_instance(self): - result = self.farmos_client.asset.get_id( - self.farmos_asset_type, - self.request.matchdict["uuid"], - params={"include": ",".join(self.get_farmos_api_includes())}, - ) + try: + result = self.farmos_client.asset.get_id( + self.farmos_asset_type, + self.request.matchdict["uuid"], + params={"include": ",".join(self.get_farmos_api_includes())}, + ) + except requests.HTTPError as exc: + if exc.response.status_code == 404: + raise self.notfound() + raise self.raw_json = result included = {obj["id"]: obj for obj in result.get("included", [])} return self.normalize_asset(result["data"], included) @@ -217,6 +239,8 @@ class AssetMasterView(FarmOSMasterView): "name": asset["attributes"]["name"], "asset_type": asset_type_object, "asset_type_name": asset_type_name, + "is_location": asset["attributes"]["is_location"], + "is_fixed": asset["attributes"]["is_fixed"], "notes": notes or colander.null, "owners": owner_objects, "owner_names": owner_names, @@ -253,6 +277,16 @@ class AssetMasterView(FarmOSMasterView): f.set_widget("notes", "notes") f.set_required("notes", False) + # is_location + f.set_node("is_location", colander.Boolean()) + + # is_fixed + f.set_node("is_fixed", colander.Boolean()) + + # groups + if self.creating or self.editing: + f.remove("groups") # TODO: add support for this? + # archived f.set_node("archived", colander.Boolean()) diff --git a/src/wuttafarm/web/views/farmos/quantities.py b/src/wuttafarm/web/views/farmos/quantities.py index 1c3830f..bd2e519 100644 --- a/src/wuttafarm/web/views/farmos/quantities.py +++ b/src/wuttafarm/web/views/farmos/quantities.py @@ -217,6 +217,7 @@ class QuantityMasterView(FarmOSMasterView): except requests.HTTPError as exc: if exc.response.status_code == 404: raise self.notfound() + raise self.raw_json = result diff --git a/src/wuttafarm/web/views/farmos/water.py b/src/wuttafarm/web/views/farmos/water.py new file mode 100644 index 0000000..129f22e --- /dev/null +++ b/src/wuttafarm/web/views/farmos/water.py @@ -0,0 +1,81 @@ +# -*- 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 Water Assets in farmOS +""" + +from wuttafarm.web.views.farmos.assets import AssetMasterView + + +class WaterAssetView(AssetMasterView): + """ + Master view for farmOS Water Assets + """ + + model_name = "farmos_water_assets" + model_title = "farmOS Water Asset" + model_title_plural = "farmOS Water Assets" + + route_prefix = "farmos_water_assets" + url_prefix = "/farmOS/assets/water" + + farmos_asset_type = "water" + farmos_refurl_path = "/assets/water" + + grid_columns = [ + "thumbnail", + "drupal_id", + "name", + "archived", + ] + + def get_xref_buttons(self, water): + model = self.app.model + session = self.Session() + buttons = super().get_xref_buttons(water) + + if wf_water := ( + session.query(model.Asset) + .filter(model.Asset.farmos_uuid == water["uuid"]) + .first() + ): + buttons.append( + self.make_button( + f"View {self.app.get_title()} record", + primary=True, + url=self.request.route_url("water_assets.view", uuid=wf_water.uuid), + icon_left="eye", + ) + ) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + WaterAssetView = kwargs.get("WaterAssetView", base["WaterAssetView"]) + WaterAssetView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wuttafarm/web/views/water.py b/src/wuttafarm/web/views/water.py new file mode 100644 index 0000000..c0d551e --- /dev/null +++ b/src/wuttafarm/web/views/water.py @@ -0,0 +1,59 @@ +# -*- 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 Water Assets +""" + +from wuttafarm.db.model import WaterAsset +from wuttafarm.web.views.assets import AssetMasterView + + +class WaterAssetView(AssetMasterView): + """ + Master view for Plant Assets + """ + + model_class = WaterAsset + route_prefix = "water_assets" + url_prefix = "/assets/water" + + farmos_bundle = "water" + farmos_refurl_path = "/assets/water" + + grid_columns = [ + "thumbnail", + "drupal_id", + "asset_name", + "parents", + "archived", + ] + + +def defaults(config, **kwargs): + base = globals() + + WaterAssetView = kwargs.get("WaterAssetView", base["WaterAssetView"]) + WaterAssetView.defaults(config) + + +def includeme(config): + defaults(config)