diff --git a/src/wuttafarm/db/alembic/versions/ea88e72a5fa5_add_units.py b/src/wuttafarm/db/alembic/versions/ea88e72a5fa5_add_units.py
new file mode 100644
index 0000000..e85afed
--- /dev/null
+++ b/src/wuttafarm/db/alembic/versions/ea88e72a5fa5_add_units.py
@@ -0,0 +1,102 @@
+"""add Units
+
+Revision ID: ea88e72a5fa5
+Revises: 82a03f4ef1a4
+Create Date: 2026-02-18 20:01:40.720138
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import wuttjamaican.db.util
+
+
+# revision identifiers, used by Alembic.
+revision: str = "ea88e72a5fa5"
+down_revision: Union[str, None] = "82a03f4ef1a4"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+
+ # unit
+ op.create_table(
+ "unit",
+ 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.Integer(), nullable=True),
+ sa.PrimaryKeyConstraint("uuid", name=op.f("pk_unit")),
+ sa.UniqueConstraint("drupal_id", name=op.f("uq_unit_drupal_id")),
+ sa.UniqueConstraint("farmos_uuid", name=op.f("uq_unit_farmos_uuid")),
+ sa.UniqueConstraint("name", name=op.f("uq_unit_name")),
+ )
+ op.create_table(
+ "unit_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.Integer(), 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_unit_version")),
+ )
+ op.create_index(
+ op.f("ix_unit_version_end_transaction_id"),
+ "unit_version",
+ ["end_transaction_id"],
+ unique=False,
+ )
+ op.create_index(
+ op.f("ix_unit_version_operation_type"),
+ "unit_version",
+ ["operation_type"],
+ unique=False,
+ )
+ op.create_index(
+ "ix_unit_version_pk_transaction_id",
+ "unit_version",
+ ["uuid", sa.literal_column("transaction_id DESC")],
+ unique=False,
+ )
+ op.create_index(
+ "ix_unit_version_pk_validity",
+ "unit_version",
+ ["uuid", "transaction_id", "end_transaction_id"],
+ unique=False,
+ )
+ op.create_index(
+ op.f("ix_unit_version_transaction_id"),
+ "unit_version",
+ ["transaction_id"],
+ unique=False,
+ )
+
+
+def downgrade() -> None:
+
+ # unit
+ op.drop_index(op.f("ix_unit_version_transaction_id"), table_name="unit_version")
+ op.drop_index("ix_unit_version_pk_validity", table_name="unit_version")
+ op.drop_index("ix_unit_version_pk_transaction_id", table_name="unit_version")
+ op.drop_index(op.f("ix_unit_version_operation_type"), table_name="unit_version")
+ op.drop_index(op.f("ix_unit_version_end_transaction_id"), table_name="unit_version")
+ op.drop_table("unit_version")
+ op.drop_table("unit")
diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py
index f9eb790..a0b856d 100644
--- a/src/wuttafarm/db/model/__init__.py
+++ b/src/wuttafarm/db/model/__init__.py
@@ -30,6 +30,7 @@ from wuttjamaican.db.model import *
from .users import WuttaFarmUser
# wuttafarm proper models
+from .unit import Unit
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/unit.py b/src/wuttafarm/db/model/unit.py
new file mode 100644
index 0000000..8cbdd5a
--- /dev/null
+++ b/src/wuttafarm/db/model/unit.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 Units
+"""
+
+import sqlalchemy as sa
+
+from wuttjamaican.db import model
+
+
+class Unit(model.Base):
+ """
+ Represents an "unit" (taxonomy term) from farmOS
+ """
+
+ __tablename__ = "unit"
+ __versioned__ = {}
+ __wutta_hint__ = {
+ "model_title": "Unit",
+ "model_title_plural": "Units",
+ }
+
+ uuid = model.uuid_column()
+
+ name = sa.Column(
+ sa.String(length=100),
+ nullable=False,
+ unique=True,
+ doc="""
+ Name of the unit.
+ """,
+ )
+
+ description = sa.Column(
+ sa.String(length=255),
+ nullable=True,
+ doc="""
+ Optional description for the unit.
+ """,
+ )
+
+ farmos_uuid = sa.Column(
+ model.UUID(),
+ nullable=True,
+ unique=True,
+ doc="""
+ UUID for the unit within farmOS.
+ """,
+ )
+
+ drupal_id = sa.Column(
+ sa.Integer(),
+ nullable=True,
+ unique=True,
+ doc="""
+ Drupal internal ID for the unit.
+ """,
+ )
+
+ def __str__(self):
+ return self.name or ""
diff --git a/src/wuttafarm/farmos/importing/model.py b/src/wuttafarm/farmos/importing/model.py
index 9bb0bf5..337649c 100644
--- a/src/wuttafarm/farmos/importing/model.py
+++ b/src/wuttafarm/farmos/importing/model.py
@@ -64,6 +64,81 @@ class ToFarmOS(Importer):
return self.app.make_utc(dt)
+class ToFarmOSTaxonomy(ToFarmOS):
+
+ farmos_taxonomy_type = None
+
+ supported_fields = [
+ "uuid",
+ "name",
+ ]
+
+ def get_target_objects(self, **kwargs):
+ result = self.farmos_client.resource.get(
+ "taxonomy_term", self.farmos_taxonomy_type
+ )
+ return result["data"]
+
+ def get_target_object(self, key):
+
+ # fetch from cache, if applicable
+ if self.caches_target:
+ return super().get_target_object(key)
+
+ # okay now must fetch via API
+ if self.get_keys() != ["uuid"]:
+ raise ValueError("must use uuid key for this to work")
+ uuid = key[0]
+
+ try:
+ result = self.farmos_client.resource.get_id(
+ "taxonomy_term", self.farmos_taxonomy_type, str(uuid)
+ )
+ except requests.HTTPError as exc:
+ if exc.response.status_code == 404:
+ return None
+ raise
+ return result["data"]
+
+ def normalize_target_object(self, obj):
+ return {
+ "uuid": UUID(obj["id"]),
+ "name": obj["attributes"]["name"],
+ }
+
+ def get_term_payload(self, source_data):
+ return {
+ "attributes": {
+ "name": source_data["name"],
+ }
+ }
+
+ def create_target_object(self, key, source_data):
+ if source_data.get("__ignoreme__"):
+ return None
+ if self.dry_run:
+ return source_data
+
+ payload = self.get_term_payload(source_data)
+ result = self.farmos_client.resource.send(
+ "taxonomy_term", self.farmos_taxonomy_type, payload
+ )
+ normal = self.normalize_target_object(result["data"])
+ normal["_new_object"] = result["data"]
+ return normal
+
+ def update_target_object(self, asset, source_data, target_data=None):
+ if self.dry_run:
+ return asset
+
+ payload = self.get_term_payload(source_data)
+ payload["id"] = str(source_data["uuid"])
+ result = self.farmos_client.resource.send(
+ "taxonomy_term", self.farmos_taxonomy_type, payload
+ )
+ return self.normalize_target_object(result["data"])
+
+
class ToFarmOSAsset(ToFarmOS):
"""
Base class for asset data importer targeting the farmOS API.
@@ -151,6 +226,12 @@ class ToFarmOSAsset(ToFarmOS):
return payload
+class UnitImporter(ToFarmOSTaxonomy):
+
+ model_title = "Unit"
+ farmos_taxonomy_type = "unit"
+
+
class AnimalAssetImporter(ToFarmOSAsset):
model_title = "AnimalAsset"
@@ -209,77 +290,10 @@ class AnimalAssetImporter(ToFarmOSAsset):
return payload
-class AnimalTypeImporter(ToFarmOS):
+class AnimalTypeImporter(ToFarmOSTaxonomy):
model_title = "AnimalType"
-
- supported_fields = [
- "uuid",
- "name",
- ]
-
- def get_target_objects(self, **kwargs):
- result = self.farmos_client.resource.get("taxonomy_term", "animal_type")
- return result["data"]
-
- def get_target_object(self, key):
-
- # fetch from cache, if applicable
- if self.caches_target:
- return super().get_target_object(key)
-
- # okay now must fetch via API
- if self.get_keys() != ["uuid"]:
- raise ValueError("must use uuid key for this to work")
- uuid = key[0]
-
- try:
- result = self.farmos_client.resource.get_id(
- "taxonomy_term", "animal_type", str(uuid)
- )
- except requests.HTTPError as exc:
- if exc.response.status_code == 404:
- return None
- raise
- return result["data"]
-
- def normalize_target_object(self, obj):
- return {
- "uuid": UUID(obj["id"]),
- "name": obj["attributes"]["name"],
- }
-
- def get_type_payload(self, source_data):
- return {
- "attributes": {
- "name": source_data["name"],
- }
- }
-
- def create_target_object(self, key, source_data):
- if source_data.get("__ignoreme__"):
- return None
- if self.dry_run:
- return source_data
-
- payload = self.get_type_payload(source_data)
- result = self.farmos_client.resource.send(
- "taxonomy_term", "animal_type", payload
- )
- normal = self.normalize_target_object(result["data"])
- normal["_new_object"] = result["data"]
- return normal
-
- def update_target_object(self, asset, source_data, target_data=None):
- if self.dry_run:
- return asset
-
- payload = self.get_type_payload(source_data)
- payload["id"] = str(source_data["uuid"])
- result = self.farmos_client.resource.send(
- "taxonomy_term", "animal_type", payload
- )
- return self.normalize_target_object(result["data"])
+ farmos_taxonomy_type = "animal_type"
class GroupAssetImporter(ToFarmOSAsset):
diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py
index 5b3a25e..e11663f 100644
--- a/src/wuttafarm/farmos/importing/wuttafarm.py
+++ b/src/wuttafarm/farmos/importing/wuttafarm.py
@@ -99,6 +99,7 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
importers["AnimalAsset"] = AnimalAssetImporter
importers["GroupAsset"] = GroupAssetImporter
importers["PlantAsset"] = PlantAssetImporter
+ importers["Unit"] = UnitImporter
importers["ActivityLog"] = ActivityLogImporter
importers["HarvestLog"] = HarvestLogImporter
importers["MedicalLog"] = MedicalLogImporter
@@ -184,6 +185,28 @@ class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporte
}
+class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter):
+ """
+ WuttaFarm → farmOS API exporter for Units
+ """
+
+ source_model_class = model.Unit
+
+ supported_fields = [
+ "uuid",
+ "name",
+ ]
+
+ drupal_internal_id_field = "drupal_internal__tid"
+
+ def normalize_source_object(self, unit):
+ return {
+ "uuid": unit.farmos_uuid or self.app.make_true_uuid(),
+ "name": unit.name,
+ "_src_object": unit,
+ }
+
+
class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter):
"""
WuttaFarm → farmOS API exporter for Group Assets
diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py
index d1cac19..fc759f5 100644
--- a/src/wuttafarm/importing/farmos.py
+++ b/src/wuttafarm/importing/farmos.py
@@ -106,6 +106,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
importers["GroupAsset"] = GroupAssetImporter
importers["PlantType"] = PlantTypeImporter
importers["PlantAsset"] = PlantAssetImporter
+ importers["Unit"] = UnitImporter
importers["LogType"] = LogTypeImporter
importers["ActivityLog"] = ActivityLogImporter
importers["HarvestLog"] = HarvestLogImporter
@@ -821,6 +822,35 @@ class UserImporter(FromFarmOS, ToWutta):
##############################
+class UnitImporter(FromFarmOS, ToWutta):
+ """
+ farmOS API → WuttaFarm importer for Units
+ """
+
+ model_class = model.Unit
+
+ supported_fields = [
+ "farmos_uuid",
+ "drupal_id",
+ "name",
+ "description",
+ ]
+
+ def get_source_objects(self):
+ """ """
+ result = self.farmos_client.resource.get("taxonomy_term", "unit")
+ return result["data"]
+
+ def normalize_source_object(self, unit):
+ """ """
+ return {
+ "farmos_uuid": UUID(unit["id"]),
+ "drupal_id": unit["attributes"]["drupal_internal__tid"],
+ "name": unit["attributes"]["name"],
+ "description": unit["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 d56977a..1e62d09 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": "Units",
+ "route": "units",
+ "perm": "units.list",
+ },
],
}
@@ -233,6 +238,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "farmos_log_types",
"perm": "farmos_log_types.list",
},
+ {
+ "title": "Units",
+ "route": "farmos_units",
+ "perm": "farmos_units.list",
+ },
{"type": "sep"},
{
"title": "Users",
diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py
index bb710a2..fa335f5 100644
--- a/src/wuttafarm/web/views/__init__.py
+++ b/src/wuttafarm/web/views/__init__.py
@@ -41,6 +41,7 @@ def includeme(config):
)
# native table views
+ config.include("wuttafarm.web.views.units")
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/common.py b/src/wuttafarm/web/views/common.py
index ce5fba2..44a9598 100644
--- a/src/wuttafarm/web/views/common.py
+++ b/src/wuttafarm/web/views/common.py
@@ -129,6 +129,11 @@ class CommonView(base.CommonView):
"structure_assets.list",
"structure_assets.view",
"structure_assets.versions",
+ "units.create",
+ "units.edit",
+ "units.list",
+ "units.view",
+ "units.versions",
]
for perm in site_admin_perms:
auth.grant_permission(site_admin, perm)
diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py
index bda5d03..c0f28a8 100644
--- a/src/wuttafarm/web/views/farmos/__init__.py
+++ b/src/wuttafarm/web/views/farmos/__init__.py
@@ -29,6 +29,7 @@ from .master import FarmOSMasterView
def includeme(config):
config.include("wuttafarm.web.views.farmos.users")
config.include("wuttafarm.web.views.farmos.asset_types")
+ config.include("wuttafarm.web.views.farmos.units")
config.include("wuttafarm.web.views.farmos.land_types")
config.include("wuttafarm.web.views.farmos.land_assets")
config.include("wuttafarm.web.views.farmos.structure_types")
diff --git a/src/wuttafarm/web/views/farmos/animal_types.py b/src/wuttafarm/web/views/farmos/animal_types.py
index 94d02d8..03bd42c 100644
--- a/src/wuttafarm/web/views/farmos/animal_types.py
+++ b/src/wuttafarm/web/views/farmos/animal_types.py
@@ -23,16 +23,10 @@
View for farmOS animal types
"""
-import datetime
-
-import colander
-
-from wuttaweb.forms.schema import WuttaDateTime
-
-from wuttafarm.web.views.farmos import FarmOSMasterView
+from wuttafarm.web.views.farmos.master import TaxonomyMasterView
-class AnimalTypeView(FarmOSMasterView):
+class AnimalTypeView(TaxonomyMasterView):
"""
Master view for Animal Types in farmOS.
"""
@@ -44,90 +38,14 @@ class AnimalTypeView(FarmOSMasterView):
route_prefix = "farmos_animal_types"
url_prefix = "/farmOS/animal-types"
+ farmos_taxonomy_type = "animal_type"
farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
- grid_columns = [
- "name",
- "description",
- "changed",
- ]
-
- sort_defaults = "name"
-
- form_fields = [
- "name",
- "description",
- "changed",
- ]
-
- def get_grid_data(self, columns=None, session=None):
- animal_types = self.farmos_client.resource.get("taxonomy_term", "animal_type")
- return [self.normalize_animal_type(t) for t in animal_types["data"]]
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
-
- # name
- g.set_link("name")
- g.set_searchable("name")
-
- # changed
- g.set_renderer("changed", "datetime")
-
- def get_instance(self):
- animal_type = self.farmos_client.resource.get_id(
- "taxonomy_term", "animal_type", self.request.matchdict["uuid"]
- )
- self.raw_json = animal_type
- return self.normalize_animal_type(animal_type["data"])
-
- def get_instance_title(self, animal_type):
- return animal_type["name"]
-
- def normalize_animal_type(self, animal_type):
-
- if changed := animal_type["attributes"]["changed"]:
- changed = datetime.datetime.fromisoformat(changed)
- changed = self.app.localtime(changed)
-
- if description := animal_type["attributes"]["description"]:
- description = description["value"]
-
- return {
- "uuid": animal_type["id"],
- "drupal_id": animal_type["attributes"]["drupal_internal__tid"],
- "name": animal_type["attributes"]["name"],
- "description": description or colander.null,
- "changed": changed,
- }
-
- def configure_form(self, form):
- f = form
- super().configure_form(f)
-
- # description
- f.set_widget("description", "notes")
-
- # changed
- f.set_node("changed", WuttaDateTime())
-
def get_xref_buttons(self, animal_type):
+ buttons = super().get_xref_buttons(animal_type)
model = self.app.model
session = self.Session()
- buttons = [
- self.make_button(
- "View in farmOS",
- primary=True,
- url=self.app.get_farmos_url(
- f"/taxonomy/term/{animal_type['drupal_id']}"
- ),
- target="_blank",
- icon_left="external-link-alt",
- )
- ]
-
if wf_animal_type := (
session.query(model.AnimalType)
.filter(model.AnimalType.farmos_uuid == animal_type["uuid"])
diff --git a/src/wuttafarm/web/views/farmos/master.py b/src/wuttafarm/web/views/farmos/master.py
index fff3d2c..56d70b6 100644
--- a/src/wuttafarm/web/views/farmos/master.py
+++ b/src/wuttafarm/web/views/farmos/master.py
@@ -23,11 +23,14 @@
Base class for farmOS master views
"""
+import datetime
import json
+import colander
import markdown
from wuttaweb.views import MasterView
+from wuttaweb.forms.schema import WuttaDateTime
from wuttafarm.web.util import save_farmos_oauth2_token
@@ -100,3 +103,90 @@ class FarmOSMasterView(MasterView):
)
return context
+
+
+class TaxonomyMasterView(FarmOSMasterView):
+ """
+ Base class for farmOS "taxonomy term" views
+ """
+
+ farmos_taxonomy_type = None
+
+ grid_columns = [
+ "name",
+ "description",
+ "changed",
+ ]
+
+ sort_defaults = "name"
+
+ form_fields = [
+ "name",
+ "description",
+ "changed",
+ ]
+
+ def get_grid_data(self, columns=None, session=None):
+ result = self.farmos_client.resource.get(
+ "taxonomy_term", self.farmos_taxonomy_type
+ )
+ return [self.normalize_taxonomy_term(t) for t in result["data"]]
+
+ def normalize_taxonomy_term(self, term):
+
+ if changed := term["attributes"]["changed"]:
+ changed = datetime.datetime.fromisoformat(changed)
+ changed = self.app.localtime(changed)
+
+ if description := term["attributes"]["description"]:
+ description = description["value"]
+
+ return {
+ "uuid": term["id"],
+ "drupal_id": term["attributes"]["drupal_internal__tid"],
+ "name": term["attributes"]["name"],
+ "description": description or colander.null,
+ "changed": changed,
+ }
+
+ def configure_grid(self, grid):
+ g = grid
+ super().configure_grid(g)
+
+ # name
+ g.set_link("name")
+ g.set_searchable("name")
+
+ # changed
+ g.set_renderer("changed", "datetime")
+
+ def get_instance(self):
+ result = self.farmos_client.resource.get_id(
+ "taxonomy_term", self.farmos_taxonomy_type, self.request.matchdict["uuid"]
+ )
+ self.raw_json = result
+ return self.normalize_taxonomy_term(result["data"])
+
+ def get_instance_title(self, term):
+ return term["name"]
+
+ def configure_form(self, form):
+ f = form
+ super().configure_form(f)
+
+ # description
+ f.set_widget("description", "notes")
+
+ # changed
+ f.set_node("changed", WuttaDateTime())
+
+ def get_xref_buttons(self, term):
+ return [
+ self.make_button(
+ "View in farmOS",
+ primary=True,
+ url=self.app.get_farmos_url(f"/taxonomy/term/{term['drupal_id']}"),
+ target="_blank",
+ icon_left="external-link-alt",
+ )
+ ]
diff --git a/src/wuttafarm/web/views/farmos/plants.py b/src/wuttafarm/web/views/farmos/plants.py
index f02801f..95a2dab 100644
--- a/src/wuttafarm/web/views/farmos/plants.py
+++ b/src/wuttafarm/web/views/farmos/plants.py
@@ -30,12 +30,13 @@ import colander
from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget
+from wuttafarm.web.views.farmos.master import TaxonomyMasterView
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import UsersType, StructureType, FarmOSPlantTypes
from wuttafarm.web.forms.widgets import ImageWidget
-class PlantTypeView(FarmOSMasterView):
+class PlantTypeView(TaxonomyMasterView):
"""
Master view for Plant Types in farmOS.
"""
@@ -47,90 +48,14 @@ class PlantTypeView(FarmOSMasterView):
route_prefix = "farmos_plant_types"
url_prefix = "/farmOS/plant-types"
+ farmos_taxonomy_type = "plant_type"
farmos_refurl_path = "/admin/structure/taxonomy/manage/plant_type/overview"
- grid_columns = [
- "name",
- "description",
- "changed",
- ]
-
- sort_defaults = "name"
-
- form_fields = [
- "name",
- "description",
- "changed",
- ]
-
- def get_grid_data(self, columns=None, session=None):
- result = self.farmos_client.resource.get("taxonomy_term", "plant_type")
- return [self.normalize_plant_type(t) for t in result["data"]]
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
-
- # name
- g.set_link("name")
- g.set_searchable("name")
-
- # changed
- g.set_renderer("changed", "datetime")
-
- def get_instance(self):
- plant_type = self.farmos_client.resource.get_id(
- "taxonomy_term", "plant_type", self.request.matchdict["uuid"]
- )
- self.raw_json = plant_type
- return self.normalize_plant_type(plant_type["data"])
-
- def get_instance_title(self, plant_type):
- return plant_type["name"]
-
- def normalize_plant_type(self, plant_type):
-
- if changed := plant_type["attributes"]["changed"]:
- changed = datetime.datetime.fromisoformat(changed)
- changed = self.app.localtime(changed)
-
- if description := plant_type["attributes"]["description"]:
- description = description["value"]
-
- return {
- "uuid": plant_type["id"],
- "drupal_id": plant_type["attributes"]["drupal_internal__tid"],
- "name": plant_type["attributes"]["name"],
- "description": description or colander.null,
- "changed": changed,
- }
-
- def configure_form(self, form):
- f = form
- super().configure_form(f)
-
- # description
- f.set_widget("description", "notes")
-
- # changed
- f.set_node("changed", WuttaDateTime())
-
def get_xref_buttons(self, plant_type):
+ buttons = super().get_xref_buttons(plant_type)
model = self.app.model
session = self.Session()
- buttons = [
- self.make_button(
- "View in farmOS",
- primary=True,
- url=self.app.get_farmos_url(
- f"/taxonomy/term/{plant_type['drupal_id']}"
- ),
- target="_blank",
- icon_left="external-link-alt",
- )
- ]
-
if wf_plant_type := (
session.query(model.PlantType)
.filter(model.PlantType.farmos_uuid == plant_type["uuid"])
diff --git a/src/wuttafarm/web/views/farmos/units.py b/src/wuttafarm/web/views/farmos/units.py
new file mode 100644
index 0000000..397614d
--- /dev/null
+++ b/src/wuttafarm/web/views/farmos/units.py
@@ -0,0 +1,74 @@
+# -*- 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 units
+"""
+
+from wuttafarm.web.views.farmos.master import TaxonomyMasterView
+
+
+class UnitView(TaxonomyMasterView):
+ """
+ Master view for Units in farmOS.
+ """
+
+ model_name = "farmos_unit"
+ model_title = "farmOS Unit"
+ model_title_plural = "farmOS Units"
+
+ route_prefix = "farmos_units"
+ url_prefix = "/farmOS/units"
+
+ farmos_taxonomy_type = "unit"
+ farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
+
+ def get_xref_buttons(self, unit):
+ buttons = super().get_xref_buttons(unit)
+ model = self.app.model
+ session = self.Session()
+
+ if wf_unit := (
+ session.query(model.Unit)
+ .filter(model.Unit.farmos_uuid == unit["uuid"])
+ .first()
+ ):
+ buttons.append(
+ self.make_button(
+ f"View {self.app.get_title()} record",
+ primary=True,
+ url=self.request.route_url("units.view", uuid=wf_unit.uuid),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ UnitView = kwargs.get("UnitView", base["UnitView"])
+ UnitView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)
diff --git a/src/wuttafarm/web/views/units.py b/src/wuttafarm/web/views/units.py
new file mode 100644
index 0000000..28570d8
--- /dev/null
+++ b/src/wuttafarm/web/views/units.py
@@ -0,0 +1,95 @@
+# -*- 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 Units
+"""
+
+from wuttafarm.web.views import WuttaFarmMasterView
+from wuttafarm.db.model import Unit
+
+
+class UnitView(WuttaFarmMasterView):
+ """
+ Master view for Units
+ """
+
+ model_class = Unit
+ route_prefix = "units"
+ url_prefix = "/animal-types"
+
+ farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
+
+ 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_farmos_url(self, unit):
+ return self.app.get_farmos_url(f"/taxonomy/term/{unit.drupal_id}")
+
+ def get_xref_buttons(self, unit):
+ buttons = super().get_xref_buttons(unit)
+
+ if unit.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_units.view", uuid=unit.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ UnitView = kwargs.get("UnitView", base["UnitView"])
+ UnitView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)