From 6204db8ae3a2b96ddae0f0c9334232f87165808e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 10 Feb 2026 19:42:02 -0600 Subject: [PATCH] feat: add native table for Log Types; import from farmOS API --- .../versions/e0d9f72575d6_add_log_types.py | 119 ++++++++++++++++++ src/wuttafarm/db/model/__init__.py | 1 + src/wuttafarm/db/model/logs.py | 82 ++++++++++++ src/wuttafarm/importing/farmos.py | 30 +++++ src/wuttafarm/web/menus.py | 24 +++- src/wuttafarm/web/views/__init__.py | 1 + src/wuttafarm/web/views/farmos/log_types.py | 21 ++++ src/wuttafarm/web/views/log_types.py | 90 +++++++++++++ 8 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 src/wuttafarm/db/alembic/versions/e0d9f72575d6_add_log_types.py create mode 100644 src/wuttafarm/db/model/logs.py create mode 100644 src/wuttafarm/web/views/log_types.py diff --git a/src/wuttafarm/db/alembic/versions/e0d9f72575d6_add_log_types.py b/src/wuttafarm/db/alembic/versions/e0d9f72575d6_add_log_types.py new file mode 100644 index 0000000..7180412 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/e0d9f72575d6_add_log_types.py @@ -0,0 +1,119 @@ +"""add Log Types + +Revision ID: e0d9f72575d6 +Revises: d7479d7161a8 +Create Date: 2026-02-10 19:35:06.631814 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "e0d9f72575d6" +down_revision: Union[str, None] = "d7479d7161a8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # log_type + op.create_table( + "log_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_internal_id", sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_type")), + sa.UniqueConstraint( + "drupal_internal_id", name=op.f("uq_log_type_drupal_internal_id") + ), + sa.UniqueConstraint("farmos_uuid", name=op.f("uq_log_type_farmos_uuid")), + sa.UniqueConstraint("name", name=op.f("uq_log_type_name")), + ) + op.create_table( + "log_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_internal_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_log_type_version") + ), + ) + op.create_index( + op.f("ix_log_type_version_end_transaction_id"), + "log_type_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_log_type_version_operation_type"), + "log_type_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_log_type_version_pk_transaction_id", + "log_type_version", + ["uuid", sa.literal_column("transaction_id DESC")], + unique=False, + ) + op.create_index( + "ix_log_type_version_pk_validity", + "log_type_version", + ["uuid", "transaction_id", "end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_log_type_version_transaction_id"), + "log_type_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # log_type + op.drop_index( + op.f("ix_log_type_version_transaction_id"), table_name="log_type_version" + ) + op.drop_index("ix_log_type_version_pk_validity", table_name="log_type_version") + op.drop_index( + "ix_log_type_version_pk_transaction_id", table_name="log_type_version" + ) + op.drop_index( + op.f("ix_log_type_version_operation_type"), table_name="log_type_version" + ) + op.drop_index( + op.f("ix_log_type_version_end_transaction_id"), table_name="log_type_version" + ) + op.drop_table("log_type_version") + op.drop_table("log_type") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index b0329de..4b5f558 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -34,3 +34,4 @@ from .assets import AssetType from .land import LandType from .structures import StructureType from .animals import AnimalType +from .logs import LogType diff --git a/src/wuttafarm/db/model/logs.py b/src/wuttafarm/db/model/logs.py new file mode 100644 index 0000000..70e3585 --- /dev/null +++ b/src/wuttafarm/db/model/logs.py @@ -0,0 +1,82 @@ +# -*- 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 Log Types +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from wuttjamaican.db import model + + +class LogType(model.Base): + """ + Represents a "log type" from farmOS + """ + + __tablename__ = "log_type" + __versioned__ = {} + __wutta_hint__ = { + "model_title": "Log Type", + "model_title_plural": "Log Types", + } + + uuid = model.uuid_column() + + name = sa.Column( + sa.String(length=100), + nullable=False, + unique=True, + doc=""" + Name of the log type. + """, + ) + + description = sa.Column( + sa.String(length=255), + nullable=True, + doc=""" + Optional description for the log type. + """, + ) + + farmos_uuid = sa.Column( + model.UUID(), + nullable=True, + unique=True, + doc=""" + UUID for the log type within farmOS. + """, + ) + + drupal_internal_id = sa.Column( + sa.String(length=50), + nullable=True, + unique=True, + doc=""" + Drupal internal ID for the log type. + """, + ) + + def __str__(self): + return self.name or "" diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 9ec0dbb..b2f6997 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -94,6 +94,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): importers["LandType"] = LandTypeImporter importers["StructureType"] = StructureTypeImporter importers["AnimalType"] = AnimalTypeImporter + importers["LogType"] = LogTypeImporter return importers @@ -214,6 +215,35 @@ class LandTypeImporter(FromFarmOS, ToWutta): } +class LogTypeImporter(FromFarmOS, ToWutta): + """ + farmOS API → WuttaFarm importer for Log Types + """ + + model_class = model.LogType + + supported_fields = [ + "farmos_uuid", + "drupal_internal_id", + "name", + "description", + ] + + def get_source_objects(self): + """ """ + log_types = self.farmos_client.resource.get("log_type") + return log_types["data"] + + def normalize_source_object(self, log_type): + """ """ + return { + "farmos_uuid": UUID(log_type["id"]), + "drupal_internal_id": log_type["attributes"]["drupal_internal__id"], + "name": log_type["attributes"]["label"], + "description": log_type["attributes"]["description"], + } + + class StructureTypeImporter(FromFarmOS, ToWutta): """ farmOS API → WuttaFarm importer for Structure Types diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index c692e7c..5bfee69 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -34,6 +34,7 @@ class WuttaFarmMenuHandler(base.MenuHandler): def make_menus(self, request, **kwargs): return [ self.make_asset_menu(request), + self.make_log_menu(request), self.make_farmos_menu(request), self.make_admin_menu(request, include_people=True), ] @@ -48,16 +49,16 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "animal_types", "perm": "animal_types.list", }, - { - "title": "Land Types", - "route": "land_types", - "perm": "land_types.list", - }, { "title": "Structure Types", "route": "structure_types", "perm": "structure_types.list", }, + { + "title": "Land Types", + "route": "land_types", + "perm": "land_types.list", + }, { "title": "Asset Types", "route": "asset_types", @@ -66,6 +67,19 @@ class WuttaFarmMenuHandler(base.MenuHandler): ], } + def make_log_menu(self, request): + return { + "title": "Logs", + "type": "menu", + "items": [ + { + "title": "Log Types", + "route": "log_types", + "perm": "log_types.list", + }, + ], + } + def make_farmos_menu(self, request): config = request.wutta_config app = config.get_app() diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index 359a0dc..412619c 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -45,6 +45,7 @@ def includeme(config): config.include("wuttafarm.web.views.land_types") config.include("wuttafarm.web.views.structure_types") config.include("wuttafarm.web.views.animal_types") + config.include("wuttafarm.web.views.log_types") # views for farmOS config.include("wuttafarm.web.views.farmos") diff --git a/src/wuttafarm/web/views/farmos/log_types.py b/src/wuttafarm/web/views/farmos/log_types.py index 6d25c10..db96fd0 100644 --- a/src/wuttafarm/web/views/farmos/log_types.py +++ b/src/wuttafarm/web/views/farmos/log_types.py @@ -87,6 +87,27 @@ class LogTypeView(FarmOSMasterView): # description f.set_widget("description", "notes") + def get_xref_buttons(self, log_type): + model = self.app.model + session = self.Session() + buttons = [] + + if wf_log_type := ( + session.query(model.LogType) + .filter(model.LogType.farmos_uuid == log_type["uuid"]) + .first() + ): + buttons.append( + self.make_button( + f"View {self.app.get_title()} record", + primary=True, + url=self.request.route_url("log_types.view", uuid=wf_log_type.uuid), + icon_left="eye", + ) + ) + + return buttons + def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/log_types.py b/src/wuttafarm/web/views/log_types.py new file mode 100644 index 0000000..5c4a8ca --- /dev/null +++ b/src/wuttafarm/web/views/log_types.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 Log Types +""" + +from wuttafarm.db.model.logs import LogType +from wuttafarm.web.views import WuttaFarmMasterView + + +class LogTypeView(WuttaFarmMasterView): + """ + Master view for Log Types + """ + + model_class = LogType + route_prefix = "log_types" + url_prefix = "/log-types" + + grid_columns = [ + "name", + "description", + ] + + sort_defaults = "name" + + filter_defaults = { + "name": {"active": True, "verb": "contains"}, + } + + form_fields = [ + "name", + "description", + "farmos_uuid", + "drupal_internal_id", + ] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + + def get_xref_buttons(self, log_type): + buttons = super().get_xref_buttons(log_type) + + if log_type.farmos_uuid: + buttons.append( + self.make_button( + "View farmOS record", + primary=True, + url=self.request.route_url( + "farmos_log_types.view", uuid=log_type.farmos_uuid + ), + icon_left="eye", + ) + ) + + return buttons + + +def defaults(config, **kwargs): + base = globals() + + LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"]) + LogTypeView.defaults(config) + + +def includeme(config): + defaults(config)