From 0a0d43aa9f86c47c81c110fc48eca91f7def52f6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 18 Feb 2026 20:13:58 -0600 Subject: [PATCH] feat: add Units table, views, import/export --- .../versions/ea88e72a5fa5_add_units.py | 102 ++++++++++++ src/wuttafarm/db/model/__init__.py | 1 + src/wuttafarm/db/model/unit.py | 81 ++++++++++ src/wuttafarm/farmos/importing/model.py | 152 ++++++++++-------- src/wuttafarm/farmos/importing/wuttafarm.py | 23 +++ src/wuttafarm/importing/farmos.py | 30 ++++ src/wuttafarm/web/menus.py | 10 ++ src/wuttafarm/web/views/__init__.py | 1 + src/wuttafarm/web/views/common.py | 5 + src/wuttafarm/web/views/farmos/__init__.py | 1 + .../web/views/farmos/animal_types.py | 90 +---------- src/wuttafarm/web/views/farmos/master.py | 90 +++++++++++ src/wuttafarm/web/views/farmos/plants.py | 83 +--------- src/wuttafarm/web/views/farmos/units.py | 74 +++++++++ src/wuttafarm/web/views/units.py | 95 +++++++++++ 15 files changed, 604 insertions(+), 234 deletions(-) create mode 100644 src/wuttafarm/db/alembic/versions/ea88e72a5fa5_add_units.py create mode 100644 src/wuttafarm/db/model/unit.py create mode 100644 src/wuttafarm/web/views/farmos/units.py create mode 100644 src/wuttafarm/web/views/units.py 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)