diff --git a/src/wuttafarm/db/alembic/versions/1f98d27cabeb_add_quantity_types.py b/src/wuttafarm/db/alembic/versions/1f98d27cabeb_add_quantity_types.py new file mode 100644 index 0000000..816f05c --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/1f98d27cabeb_add_quantity_types.py @@ -0,0 +1,119 @@ +"""add Quantity Types + +Revision ID: 1f98d27cabeb +Revises: ea88e72a5fa5 +Create Date: 2026-02-18 21:03:52.245619 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "1f98d27cabeb" +down_revision: Union[str, None] = "ea88e72a5fa5" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # quantity_type + op.create_table( + "quantity_type", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("description", sa.String(length=255), nullable=True), + sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True), + sa.Column("drupal_id", sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity_type")), + sa.UniqueConstraint("drupal_id", name=op.f("uq_quantity_type_drupal_id")), + sa.UniqueConstraint("farmos_uuid", name=op.f("uq_quantity_type_farmos_uuid")), + sa.UniqueConstraint("name", name=op.f("uq_quantity_type_name")), + ) + op.create_table( + "quantity_type_version", + sa.Column( + "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False + ), + sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True), + sa.Column( + "description", sa.String(length=255), autoincrement=False, nullable=True + ), + sa.Column( + "farmos_uuid", + wuttjamaican.db.util.UUID(), + autoincrement=False, + nullable=True, + ), + sa.Column( + "drupal_id", sa.String(length=50), 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_quantity_type_version") + ), + ) + op.create_index( + op.f("ix_quantity_type_version_end_transaction_id"), + "quantity_type_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_quantity_type_version_operation_type"), + "quantity_type_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_quantity_type_version_pk_transaction_id", + "quantity_type_version", + ["uuid", sa.literal_column("transaction_id DESC")], + unique=False, + ) + op.create_index( + "ix_quantity_type_version_pk_validity", + "quantity_type_version", + ["uuid", "transaction_id", "end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_quantity_type_version_transaction_id"), + "quantity_type_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # quantity_type + op.drop_index( + op.f("ix_quantity_type_version_transaction_id"), + table_name="quantity_type_version", + ) + op.drop_index( + "ix_quantity_type_version_pk_validity", table_name="quantity_type_version" + ) + op.drop_index( + "ix_quantity_type_version_pk_transaction_id", table_name="quantity_type_version" + ) + op.drop_index( + op.f("ix_quantity_type_version_operation_type"), + table_name="quantity_type_version", + ) + op.drop_index( + op.f("ix_quantity_type_version_end_transaction_id"), + table_name="quantity_type_version", + ) + op.drop_table("quantity_type_version") + op.drop_table("quantity_type") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index a0b856d..827fc70 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -31,6 +31,7 @@ from .users import WuttaFarmUser # wuttafarm proper models from .unit import Unit +from .quantities import QuantityType from .asset import AssetType, Asset, AssetParent from .asset_land import LandType, LandAsset from .asset_structure import StructureType, StructureAsset diff --git a/src/wuttafarm/db/model/quantities.py b/src/wuttafarm/db/model/quantities.py new file mode 100644 index 0000000..b66f9bb --- /dev/null +++ b/src/wuttafarm/db/model/quantities.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 . +# +################################################################################ +""" +Model definition for Quantities +""" + +import sqlalchemy as sa + +from wuttjamaican.db import model + + +class QuantityType(model.Base): + """ + Represents an "quantity type" from farmOS + """ + + __tablename__ = "quantity_type" + __versioned__ = {} + __wutta_hint__ = { + "model_title": "Quantity Type", + "model_title_plural": "Quantity Types", + } + + uuid = model.uuid_column() + + name = sa.Column( + sa.String(length=100), + nullable=False, + unique=True, + doc=""" + Name of the quantity type. + """, + ) + + description = sa.Column( + sa.String(length=255), + nullable=True, + doc=""" + Description for the quantity type. + """, + ) + + farmos_uuid = sa.Column( + model.UUID(), + nullable=True, + unique=True, + doc=""" + UUID for the quantity type within farmOS. + """, + ) + + drupal_id = sa.Column( + sa.String(length=50), + nullable=True, + unique=True, + doc=""" + Drupal internal ID for the quantity type. + """, + ) + + def __str__(self): + return self.name or "" diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index fc759f5..90a4a7c 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -107,6 +107,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): importers["PlantType"] = PlantTypeImporter importers["PlantAsset"] = PlantAssetImporter importers["Unit"] = UnitImporter + importers["QuantityType"] = QuantityTypeImporter importers["LogType"] = LogTypeImporter importers["ActivityLog"] = ActivityLogImporter importers["HarvestLog"] = HarvestLogImporter @@ -851,6 +852,35 @@ class UnitImporter(FromFarmOS, ToWutta): } +class QuantityTypeImporter(FromFarmOS, ToWutta): + """ + farmOS API → WuttaFarm importer for Quantity Types + """ + + model_class = model.QuantityType + + supported_fields = [ + "farmos_uuid", + "drupal_id", + "name", + "description", + ] + + def get_source_objects(self): + """ """ + result = self.farmos_client.resource.get("quantity_type") + return result["data"] + + def normalize_source_object(self, quantity_type): + """ """ + return { + "farmos_uuid": UUID(quantity_type["id"]), + "drupal_id": quantity_type["attributes"]["drupal_internal__id"], + "name": quantity_type["attributes"]["label"], + "description": quantity_type["attributes"]["description"], + } + + class LogTypeImporter(FromFarmOS, ToWutta): """ farmOS API → WuttaFarm importer for Log Types diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 1e62d09..01e0f07 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -139,6 +139,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "log_types", "perm": "log_types.list", }, + { + "title": "Quantity Types", + "route": "quantity_types", + "perm": "quantity_types.list", + }, { "title": "Units", "route": "units", @@ -238,6 +243,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_log_types", "perm": "farmos_log_types.list", }, + { + "title": "Quantity Types", + "route": "farmos_quantity_types", + "perm": "farmos_quantity_types.list", + }, { "title": "Units", "route": "farmos_units", diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index fa335f5..21dcbad 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -42,6 +42,7 @@ def includeme(config): # native table views config.include("wuttafarm.web.views.units") + config.include("wuttafarm.web.views.quantities") config.include("wuttafarm.web.views.asset_types") config.include("wuttafarm.web.views.assets") config.include("wuttafarm.web.views.land") diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index c0f28a8..cfedfb1 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -28,6 +28,7 @@ from .master import FarmOSMasterView def includeme(config): config.include("wuttafarm.web.views.farmos.users") + config.include("wuttafarm.web.views.farmos.quantity_types") config.include("wuttafarm.web.views.farmos.asset_types") config.include("wuttafarm.web.views.farmos.units") config.include("wuttafarm.web.views.farmos.land_types") diff --git a/src/wuttafarm/web/views/farmos/quantity_types.py b/src/wuttafarm/web/views/farmos/quantity_types.py new file mode 100644 index 0000000..2b10a0a --- /dev/null +++ b/src/wuttafarm/web/views/farmos/quantity_types.py @@ -0,0 +1,125 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS Quantity Types +""" + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class QuantityTypeView(FarmOSMasterView): + """ + View for farmOS Quantity Types + """ + + model_name = "farmos_quantity_type" + model_title = "farmOS Quantity Type" + model_title_plural = "farmOS Quantity Types" + + route_prefix = "farmos_quantity_types" + url_prefix = "/farmOS/quantity-types" + + grid_columns = [ + "label", + "description", + ] + + sort_defaults = "label" + + form_fields = [ + "label", + "description", + ] + + def get_grid_data(self, columns=None, session=None): + result = self.farmos_client.resource.get("quantity_type") + return [self.normalize_quantity_type(t) for t in result["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # label + g.set_link("label") + g.set_searchable("label") + + # description + g.set_searchable("description") + + def get_instance(self): + result = self.farmos_client.resource.get_id( + "quantity_type", "quantity_type", self.request.matchdict["uuid"] + ) + self.raw_json = result + return self.normalize_quantity_type(result["data"]) + + def get_instance_title(self, quantity_type): + return quantity_type["label"] + + def normalize_quantity_type(self, quantity_type): + return { + "uuid": quantity_type["id"], + "drupal_id": quantity_type["attributes"]["drupal_internal__id"], + "label": quantity_type["attributes"]["label"], + "description": quantity_type["attributes"]["description"], + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # description + f.set_widget("description", "notes") + + def get_xref_buttons(self, quantity_type): + model = self.app.model + session = self.Session() + buttons = [] + + if wf_quantity_type := ( + session.query(model.QuantityType) + .filter(model.QuantityType.farmos_uuid == quantity_type["uuid"]) + .first() + ): + buttons.append( + self.make_button( + f"View {self.app.get_title()} record", + primary=True, + url=self.request.route_url( + "quantity_types.view", uuid=wf_quantity_type.uuid + ), + icon_left="eye", + ) + ) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"]) + QuantityTypeView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wuttafarm/web/views/quantities.py b/src/wuttafarm/web/views/quantities.py new file mode 100644 index 0000000..1291791 --- /dev/null +++ b/src/wuttafarm/web/views/quantities.py @@ -0,0 +1,90 @@ +# -*- 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 Quantities +""" + +from wuttafarm.web.views import WuttaFarmMasterView +from wuttafarm.db.model import QuantityType + + +class QuantityTypeView(WuttaFarmMasterView): + """ + Master view for Quantity Types + """ + + model_class = QuantityType + route_prefix = "quantity_types" + url_prefix = "/quantity-types" + + grid_columns = [ + "name", + "description", + ] + + sort_defaults = "name" + + filter_defaults = { + "name": {"active": True, "verb": "contains"}, + } + + form_fields = [ + "name", + "description", + "farmos_uuid", + "drupal_id", + ] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + + def get_xref_buttons(self, quantity_type): + buttons = super().get_xref_buttons(quantity_type) + + if quantity_type.farmos_uuid: + buttons.append( + self.make_button( + "View farmOS record", + primary=True, + url=self.request.route_url( + "farmos_quantity_types.view", uuid=quantity_type.farmos_uuid + ), + icon_left="eye", + ) + ) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"]) + QuantityTypeView.defaults(config) + + +def includeme(config): + defaults(config)