diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e7712a..2111345 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,35 +5,6 @@ All notable changes to WuttaFarm will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## v0.4.0 (2026-02-17)
-
-### Feat
-
-- add basic support for WuttaFarm → farmOS export
-- convert group assets to use common base/mixin
-- convert structure assets to use common base/mixin
-- convert land assets to use common base/mixin
-- add "generic" assets, new animal assets based on that
-
-### Fix
-
-- misc. field tweaks for asset forms
-- show warning when viewing an archived asset
-- fix some perms for all assets view
-- fix initial admin perms per route renaming
-- add parent relationships support for land assets
-- cleanup Land views to better match farmOS
-- cleanup Structure views to better match farmOS
-- cleanup Group views to better match farmOS
-- add / display thumbnail image for animals
-- improve handling of 'archived' records for grid/form views
-- use Male/Female dict enum for animal sex field
-- prevent direct edit of `farmos_uuid` and `drupal_id` fields
-- use same datetime display format as farmOS
-- convert `active` flag to `archived`
-- suppress output when user farmos/drupal keys are empty
-- customize page footer to mention farmOS
-
## v0.3.1 (2026-02-14)
### Fix
diff --git a/pyproject.toml b/pyproject.toml
index 12bce62..073879b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaFarm"
-version = "0.4.0"
+version = "0.3.1"
description = "Web app to integrate with and extend farmOS"
readme = "README.md"
authors = [
@@ -34,7 +34,7 @@ dependencies = [
"pyramid_exclog",
"uvicorn[standard]",
"WuttaSync",
- "WuttaWeb[continuum]>=0.28.1",
+ "WuttaWeb[continuum]>=0.27.4",
]
@@ -58,7 +58,6 @@ wuttafarm = "wuttafarm.app:WuttaFarmAppProvider"
"wuttafarm" = "wuttafarm.web.menus:WuttaFarmMenuHandler"
[project.entry-points."wuttasync.importing"]
-"export.to_farmos.from_wuttafarm" = "wuttafarm.farmos.importing.wuttafarm:FromWuttaFarmToFarmOS"
"import.to_wuttafarm.from_farmos" = "wuttafarm.importing.farmos:FromFarmOSToWuttaFarm"
diff --git a/src/wuttafarm/app.py b/src/wuttafarm/app.py
index 087c48a..9cfe25d 100644
--- a/src/wuttafarm/app.py
+++ b/src/wuttafarm/app.py
@@ -31,8 +31,6 @@ class WuttaFarmAppHandler(base.AppHandler):
Custom :term:`app handler` for WuttaFarm.
"""
- display_format_datetime = "%a, %m/%d/%Y - %H:%M"
-
default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler"
@@ -85,38 +83,6 @@ class WuttaFarmAppHandler(base.AppHandler):
handler = self.get_farmos_handler()
return handler.is_farmos_4x(*args, **kwargs)
- def export_to_farmos(self, obj, require=True):
- """
- Export the given object to farmOS, using configured handler.
-
- This should ensure the given object is also *updated* with the
- farmOS UUID and Drupal ID, when new record is created in
- farmOS.
-
- :param obj: Any data object in WuttaFarm, e.g. AnimalAsset
- instance.
-
- :param require: If true, this will *require* the export
- handler to support objects of the given type. If false,
- then nothing will happen / export is silently skipped when
- there is no such exporter.
- """
- handler = self.app.get_import_handler("export.to_farmos.from_wuttafarm")
-
- model_name = type(obj).__name__
- if model_name not in handler.importers:
- if require:
- raise ValueError(f"no exporter found for {model_name}")
- return
-
- # nb. begin txn to establish the API client
- # TODO: should probably use current user oauth2 token instead
- # of always making a new one here, which is what happens IIUC
- handler.begin_target_transaction()
- importer = handler.get_importer(model_name, caches_target=False)
- normal = importer.normalize_source_object(obj)
- importer.process_data(source_data=[normal])
-
class WuttaFarmAppProvider(base.AppProvider):
"""
diff --git a/src/wuttafarm/cli/__init__.py b/src/wuttafarm/cli/__init__.py
index cd06344..7f6c2bb 100644
--- a/src/wuttafarm/cli/__init__.py
+++ b/src/wuttafarm/cli/__init__.py
@@ -26,6 +26,5 @@ WuttaFarm CLI
from .base import wuttafarm_typer
# nb. must bring in all modules for discovery to work
-from . import export_farmos
from . import import_farmos
from . import install
diff --git a/src/wuttafarm/cli/export_farmos.py b/src/wuttafarm/cli/export_farmos.py
deleted file mode 100644
index 18a21dd..0000000
--- a/src/wuttafarm/cli/export_farmos.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- 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 .
-#
-################################################################################
-"""
-See also: :ref:`wuttafarm-export-farmos`
-"""
-
-import typer
-
-from wuttasync.cli import import_command, ImportCommandHandler
-
-from wuttafarm.cli import wuttafarm_typer
-
-
-@wuttafarm_typer.command()
-@import_command
-def export_farmos(ctx: typer.Context, **kwargs):
- """
- Export data from WuttaFarm to farmOS API
- """
- config = ctx.parent.wutta_config
- handler = ImportCommandHandler(config, key="export.to_farmos.from_wuttafarm")
- handler.run(ctx)
diff --git a/src/wuttafarm/config.py b/src/wuttafarm/config.py
index 5828299..fcc8aae 100644
--- a/src/wuttafarm/config.py
+++ b/src/wuttafarm/config.py
@@ -39,9 +39,8 @@ class WuttaFarmConfig(WuttaConfigExtension):
config.setdefault(f"{config.appname}.app_title", "WuttaFarm")
config.setdefault(f"{config.appname}.app_dist", "WuttaFarm")
- # app model/enum
+ # app model
config.setdefault(f"{config.appname}.model_spec", "wuttafarm.db.model")
- config.setdefault(f"{config.appname}.enum_spec", "wuttafarm.enum")
# app handler
config.setdefault(
diff --git a/src/wuttafarm/db/alembic/versions/2a49127e974b_add_animal_thumbnail_url.py b/src/wuttafarm/db/alembic/versions/2a49127e974b_add_animal_thumbnail_url.py
deleted file mode 100644
index 7c32b29..0000000
--- a/src/wuttafarm/db/alembic/versions/2a49127e974b_add_animal_thumbnail_url.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""add animal thumbnail url
-
-Revision ID: 2a49127e974b
-Revises: 8898184c5c75
-Create Date: 2026-02-14 19:41:22.039343
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "2a49127e974b"
-down_revision: Union[str, None] = "8898184c5c75"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # animal
- op.add_column(
- "animal", sa.Column("thumbnail_url", sa.String(length=255), nullable=True)
- )
- op.add_column(
- "animal_version",
- sa.Column(
- "thumbnail_url", sa.String(length=255), autoincrement=False, nullable=True
- ),
- )
-
-
-def downgrade() -> None:
-
- # animal
- op.drop_column("animal_version", "thumbnail_url")
- op.drop_column("animal", "thumbnail_url")
diff --git a/src/wuttafarm/db/alembic/versions/34ec51d80f52_use_shared_base_for_structure_assets.py b/src/wuttafarm/db/alembic/versions/34ec51d80f52_use_shared_base_for_structure_assets.py
deleted file mode 100644
index 4be383f..0000000
--- a/src/wuttafarm/db/alembic/versions/34ec51d80f52_use_shared_base_for_structure_assets.py
+++ /dev/null
@@ -1,236 +0,0 @@
-"""use shared base for Structure Assets
-
-Revision ID: 34ec51d80f52
-Revises: d882682c82f9
-Create Date: 2026-02-15 13:19:18.814523
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "34ec51d80f52"
-down_revision: Union[str, None] = "d882682c82f9"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # asset_structure
- op.create_table(
- "asset_structure",
- sa.Column("structure_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.ForeignKeyConstraint(
- ["structure_type_uuid"],
- ["structure_type.uuid"],
- name=op.f("fk_asset_structure_structure_type_uuid_structure_type"),
- ),
- sa.ForeignKeyConstraint(
- ["uuid"], ["asset.uuid"], name=op.f("fk_asset_structure_uuid_asset")
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_structure")),
- )
- op.create_table(
- "asset_structure_version",
- sa.Column(
- "structure_type_uuid",
- wuttjamaican.db.util.UUID(),
- autoincrement=False,
- nullable=True,
- ),
- 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_structure_version")
- ),
- )
- op.create_index(
- op.f("ix_asset_structure_version_end_transaction_id"),
- "asset_structure_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_structure_version_operation_type"),
- "asset_structure_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_asset_structure_version_pk_transaction_id",
- "asset_structure_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_asset_structure_version_pk_validity",
- "asset_structure_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_structure_version_transaction_id"),
- "asset_structure_version",
- ["transaction_id"],
- unique=False,
- )
-
- # structure
- op.drop_index(
- op.f("ix_structure_version_end_transaction_id"), table_name="structure_version"
- )
- op.drop_index(
- op.f("ix_structure_version_operation_type"), table_name="structure_version"
- )
- op.drop_index(
- op.f("ix_structure_version_pk_transaction_id"), table_name="structure_version"
- )
- op.drop_index(
- op.f("ix_structure_version_pk_validity"), table_name="structure_version"
- )
- op.drop_index(
- op.f("ix_structure_version_transaction_id"), table_name="structure_version"
- )
- op.drop_table("structure_version")
- op.drop_table("structure")
-
-
-def downgrade() -> None:
-
- # structure
- op.create_table(
- "structure",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column(
- "structure_type_uuid", sa.UUID(), autoincrement=False, nullable=False
- ),
- sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column(
- "image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.Column(
- "thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.ForeignKeyConstraint(
- ["structure_type_uuid"],
- ["structure_type.uuid"],
- name=op.f("fk_structure_structure_type_uuid_structure_type"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_structure")),
- sa.UniqueConstraint(
- "drupal_id",
- name=op.f("uq_structure_drupal_id"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "farmos_uuid",
- name=op.f("uq_structure_farmos_uuid"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "name",
- name=op.f("uq_structure_name"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- )
- op.create_table(
- "structure_version",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("structure_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column(
- "image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
- sa.Column(
- "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
- ),
- sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
- sa.Column(
- "thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.PrimaryKeyConstraint(
- "uuid", "transaction_id", name=op.f("pk_structure_version")
- ),
- )
- op.create_index(
- op.f("ix_structure_version_transaction_id"),
- "structure_version",
- ["transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_structure_version_pk_validity"),
- "structure_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_structure_version_pk_transaction_id"),
- "structure_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- op.f("ix_structure_version_operation_type"),
- "structure_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- op.f("ix_structure_version_end_transaction_id"),
- "structure_version",
- ["end_transaction_id"],
- unique=False,
- )
-
- # asset_structure
- op.drop_index(
- op.f("ix_asset_structure_version_transaction_id"),
- table_name="asset_structure_version",
- )
- op.drop_index(
- "ix_asset_structure_version_pk_validity", table_name="asset_structure_version"
- )
- op.drop_index(
- "ix_asset_structure_version_pk_transaction_id",
- table_name="asset_structure_version",
- )
- op.drop_index(
- op.f("ix_asset_structure_version_operation_type"),
- table_name="asset_structure_version",
- )
- op.drop_index(
- op.f("ix_asset_structure_version_end_transaction_id"),
- table_name="asset_structure_version",
- )
- op.drop_table("asset_structure_version")
- op.drop_table("asset_structure")
diff --git a/src/wuttafarm/db/alembic/versions/554e6168c339_add_landassetparent_model.py b/src/wuttafarm/db/alembic/versions/554e6168c339_add_landassetparent_model.py
deleted file mode 100644
index e943f77..0000000
--- a/src/wuttafarm/db/alembic/versions/554e6168c339_add_landassetparent_model.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""add LandAssetParent model
-
-Revision ID: 554e6168c339
-Revises: 8cc1565d38e7
-Create Date: 2026-02-14 20:41:24.859064
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "554e6168c339"
-down_revision: Union[str, None] = "8cc1565d38e7"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # land_asset_parent
- op.create_table(
- "land_asset_parent",
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("land_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("parent_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.ForeignKeyConstraint(
- ["land_asset_uuid"],
- ["land_asset.uuid"],
- name=op.f("fk_land_asset_parent_land_asset_uuid_land_asset"),
- ),
- sa.ForeignKeyConstraint(
- ["parent_asset_uuid"],
- ["land_asset.uuid"],
- name=op.f("fk_land_asset_parent_parent_asset_uuid_land_asset"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset_parent")),
- )
- op.create_table(
- "land_asset_parent_version",
- sa.Column(
- "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
- ),
- sa.Column(
- "land_asset_uuid",
- wuttjamaican.db.util.UUID(),
- autoincrement=False,
- nullable=True,
- ),
- sa.Column(
- "parent_asset_uuid",
- wuttjamaican.db.util.UUID(),
- 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_land_asset_parent_version")
- ),
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_end_transaction_id"),
- "land_asset_parent_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_operation_type"),
- "land_asset_parent_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_land_asset_parent_version_pk_transaction_id",
- "land_asset_parent_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_land_asset_parent_version_pk_validity",
- "land_asset_parent_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_transaction_id"),
- "land_asset_parent_version",
- ["transaction_id"],
- unique=False,
- )
-
-
-def downgrade() -> None:
-
- # land_asset_parent
- op.drop_index(
- op.f("ix_land_asset_parent_version_transaction_id"),
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- "ix_land_asset_parent_version_pk_validity",
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- "ix_land_asset_parent_version_pk_transaction_id",
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- op.f("ix_land_asset_parent_version_operation_type"),
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- op.f("ix_land_asset_parent_version_end_transaction_id"),
- table_name="land_asset_parent_version",
- )
- op.drop_table("land_asset_parent_version")
- op.drop_table("land_asset_parent")
diff --git a/src/wuttafarm/db/alembic/versions/8898184c5c75_convert_active_to_archived.py b/src/wuttafarm/db/alembic/versions/8898184c5c75_convert_active_to_archived.py
deleted file mode 100644
index 70bbe2c..0000000
--- a/src/wuttafarm/db/alembic/versions/8898184c5c75_convert_active_to_archived.py
+++ /dev/null
@@ -1,250 +0,0 @@
-"""convert active to archived
-
-Revision ID: 8898184c5c75
-Revises: 3e2ef02bf264
-Create Date: 2026-02-14 18:41:23.042951
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "8898184c5c75"
-down_revision: Union[str, None] = "3e2ef02bf264"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # animal
- op.alter_column("animal", "active", new_column_name="archived")
- animal = sa.sql.table(
- "animal",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(animal.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- animal.update()
- .where(animal.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
- op.alter_column("animal_version", "active", new_column_name="archived")
- animal_version = sa.sql.table(
- "animal_version",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(animal_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- animal_version.update()
- .where(animal_version.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
-
- # group
- op.alter_column("group", "active", new_column_name="archived")
- group = sa.sql.table(
- "group",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(group.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- group.update()
- .where(group.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
- op.alter_column("group_version", "active", new_column_name="archived")
- group_version = sa.sql.table(
- "group_version",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(group_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- group_version.update()
- .where(group_version.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
-
- # land_asset
- op.alter_column("land_asset", "active", new_column_name="archived")
- land_asset = sa.sql.table(
- "land_asset",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(land_asset.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- land_asset.update()
- .where(land_asset.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
- op.alter_column("land_asset_version", "active", new_column_name="archived")
- land_asset_version = sa.sql.table(
- "land_asset_version",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(land_asset_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- land_asset_version.update()
- .where(land_asset_version.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
-
- # structure
- op.alter_column("structure", "active", new_column_name="archived")
- structure = sa.sql.table(
- "structure",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(structure.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- structure.update()
- .where(structure.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
- op.alter_column("structure_version", "active", new_column_name="archived")
- structure_version = sa.sql.table(
- "structure_version",
- sa.sql.column("uuid"),
- sa.sql.column("archived"),
- )
- cursor = op.get_bind().execute(structure_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- structure_version.update()
- .where(structure_version.c.uuid == row.uuid)
- .values({"archived": not row.archived})
- )
-
-
-def downgrade() -> None:
-
- # structure
- op.alter_column("structure", "archived", new_column_name="active")
- structure = sa.sql.table(
- "structure",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(structure.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- structure.update()
- .where(structure.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
- op.alter_column("structure_version", "archived", new_column_name="active")
- structure_version = sa.sql.table(
- "structure_version",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(structure_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- structure_version.update()
- .where(structure_version.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
-
- # land_asset
- op.alter_column("land_asset", "archived", new_column_name="active")
- land_asset = sa.sql.table(
- "land_asset",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(land_asset.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- land_asset.update()
- .where(land_asset.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
- op.alter_column("land_asset_version", "archived", new_column_name="active")
- land_asset_version = sa.sql.table(
- "land_asset_version",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(land_asset_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- land_asset_version.update()
- .where(land_asset_version.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
-
- # group
- op.alter_column("group", "archived", new_column_name="active")
- group = sa.sql.table(
- "group",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(group.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- group.update()
- .where(group.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
- op.alter_column("group_version", "archived", new_column_name="active")
- group_version = sa.sql.table(
- "group_version",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(group_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- group_version.update()
- .where(group_version.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
-
- # animal
- op.alter_column("animal", "archived", new_column_name="active")
- animal = sa.sql.table(
- "animal",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(animal.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- animal.update()
- .where(animal.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
- op.alter_column("animal_version", "archived", new_column_name="active")
- animal_version = sa.sql.table(
- "animal_version",
- sa.sql.column("uuid"),
- sa.sql.column("active"),
- )
- cursor = op.get_bind().execute(animal_version.select())
- for row in cursor.fetchall():
- op.get_bind().execute(
- animal_version.update()
- .where(animal_version.c.uuid == row.uuid)
- .values({"active": not row.active})
- )
diff --git a/src/wuttafarm/db/alembic/versions/8cc1565d38e7_add_structure_thumbnail_url.py b/src/wuttafarm/db/alembic/versions/8cc1565d38e7_add_structure_thumbnail_url.py
deleted file mode 100644
index 6bcd51b..0000000
--- a/src/wuttafarm/db/alembic/versions/8cc1565d38e7_add_structure_thumbnail_url.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""add structure thumbnail url
-
-Revision ID: 8cc1565d38e7
-Revises: 2a49127e974b
-Create Date: 2026-02-14 20:07:33.913573
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "8cc1565d38e7"
-down_revision: Union[str, None] = "2a49127e974b"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # structure
- op.add_column(
- "structure", sa.Column("thumbnail_url", sa.String(length=255), nullable=True)
- )
- op.add_column(
- "structure_version",
- sa.Column(
- "thumbnail_url", sa.String(length=255), autoincrement=False, nullable=True
- ),
- )
-
-
-def downgrade() -> None:
-
- # structure
- op.drop_column("structure_version", "thumbnail_url")
- op.drop_column("structure", "thumbnail_url")
diff --git a/src/wuttafarm/db/alembic/versions/aecfd9175624_use_shared_base_for_group_assets.py b/src/wuttafarm/db/alembic/versions/aecfd9175624_use_shared_base_for_group_assets.py
deleted file mode 100644
index 1295d40..0000000
--- a/src/wuttafarm/db/alembic/versions/aecfd9175624_use_shared_base_for_group_assets.py
+++ /dev/null
@@ -1,194 +0,0 @@
-"""use shared base for Group Assets
-
-Revision ID: aecfd9175624
-Revises: 34ec51d80f52
-Create Date: 2026-02-15 13:57:01.055304
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "aecfd9175624"
-down_revision: Union[str, None] = "34ec51d80f52"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # asset_group
- op.create_table(
- "asset_group",
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.ForeignKeyConstraint(
- ["uuid"], ["asset.uuid"], name=op.f("fk_asset_group_uuid_asset")
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_group")),
- )
- op.create_table(
- "asset_group_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_group_version")
- ),
- )
- op.create_index(
- op.f("ix_asset_group_version_end_transaction_id"),
- "asset_group_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_group_version_operation_type"),
- "asset_group_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_asset_group_version_pk_transaction_id",
- "asset_group_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_asset_group_version_pk_validity",
- "asset_group_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_group_version_transaction_id"),
- "asset_group_version",
- ["transaction_id"],
- unique=False,
- )
-
- # group
- op.drop_index(
- op.f("ix_group_version_end_transaction_id"), table_name="group_version"
- )
- op.drop_index(op.f("ix_group_version_operation_type"), table_name="group_version")
- op.drop_index(
- op.f("ix_group_version_pk_transaction_id"), table_name="group_version"
- )
- op.drop_index(op.f("ix_group_version_pk_validity"), table_name="group_version")
- op.drop_index(op.f("ix_group_version_transaction_id"), table_name="group_version")
- op.drop_table("group_version")
- op.drop_table("group")
-
-
-def downgrade() -> None:
-
- # group
- op.create_table(
- "group",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
- sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_group")),
- sa.UniqueConstraint(
- "drupal_id",
- name=op.f("uq_group_drupal_id"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "farmos_uuid",
- name=op.f("uq_group_farmos_uuid"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "name",
- name=op.f("uq_group_name"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- )
- op.create_table(
- "group_version",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
- sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
- sa.Column(
- "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
- ),
- sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint(
- "uuid", "transaction_id", name=op.f("pk_group_version")
- ),
- )
- op.create_index(
- op.f("ix_group_version_transaction_id"),
- "group_version",
- ["transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_group_version_pk_validity"),
- "group_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_group_version_pk_transaction_id"),
- "group_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- op.f("ix_group_version_operation_type"),
- "group_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- op.f("ix_group_version_end_transaction_id"),
- "group_version",
- ["end_transaction_id"],
- unique=False,
- )
-
- # asset_group
- op.drop_index(
- op.f("ix_asset_group_version_transaction_id"), table_name="asset_group_version"
- )
- op.drop_index(
- "ix_asset_group_version_pk_validity", table_name="asset_group_version"
- )
- op.drop_index(
- "ix_asset_group_version_pk_transaction_id", table_name="asset_group_version"
- )
- op.drop_index(
- op.f("ix_asset_group_version_operation_type"), table_name="asset_group_version"
- )
- op.drop_index(
- op.f("ix_asset_group_version_end_transaction_id"),
- table_name="asset_group_version",
- )
- op.drop_table("asset_group_version")
- op.drop_table("asset_group")
diff --git a/src/wuttafarm/db/alembic/versions/d6e8d16d6854_add_generic_animal_assets.py b/src/wuttafarm/db/alembic/versions/d6e8d16d6854_add_generic_animal_assets.py
deleted file mode 100644
index cd0a34a..0000000
--- a/src/wuttafarm/db/alembic/versions/d6e8d16d6854_add_generic_animal_assets.py
+++ /dev/null
@@ -1,333 +0,0 @@
-"""add generic, animal assets
-
-Revision ID: d6e8d16d6854
-Revises: 554e6168c339
-Create Date: 2026-02-15 09:11:04.886362
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "d6e8d16d6854"
-down_revision: Union[str, None] = "554e6168c339"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # animal
- op.drop_table("animal")
- op.drop_index(
- op.f("ix_animal_version_end_transaction_id"), table_name="animal_version"
- )
- op.drop_index(op.f("ix_animal_version_operation_type"), table_name="animal_version")
- op.drop_index(
- op.f("ix_animal_version_pk_transaction_id"), table_name="animal_version"
- )
- op.drop_index(op.f("ix_animal_version_pk_validity"), table_name="animal_version")
- op.drop_index(op.f("ix_animal_version_transaction_id"), table_name="animal_version")
- op.drop_table("animal_version")
-
- # asset
- op.create_table(
- "asset",
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True),
- sa.Column("drupal_id", sa.Integer(), nullable=True),
- sa.Column("asset_type", sa.String(length=100), nullable=False),
- sa.Column("asset_name", sa.String(length=100), nullable=False),
- sa.Column("is_location", sa.Boolean(), nullable=False),
- sa.Column("is_fixed", sa.Boolean(), nullable=False),
- sa.Column("notes", sa.Text(), nullable=True),
- sa.Column("thumbnail_url", sa.String(length=255), nullable=True),
- sa.Column("image_url", sa.String(length=255), nullable=True),
- sa.Column("archived", sa.Boolean(), nullable=False),
- sa.ForeignKeyConstraint(
- ["asset_type"],
- ["asset_type.drupal_id"],
- name=op.f("fk_asset_asset_type_asset_type"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset")),
- sa.UniqueConstraint("drupal_id", name=op.f("uq_asset_drupal_id")),
- sa.UniqueConstraint("farmos_uuid", name=op.f("uq_asset_farmos_uuid")),
- )
- op.create_table(
- "asset_version",
- sa.Column(
- "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
- ),
- sa.Column(
- "farmos_uuid",
- wuttjamaican.db.util.UUID(),
- autoincrement=False,
- nullable=True,
- ),
- sa.Column("drupal_id", sa.Integer(), autoincrement=False, nullable=True),
- sa.Column(
- "asset_type", sa.String(length=100), autoincrement=False, nullable=True
- ),
- sa.Column(
- "asset_name", sa.String(length=100), autoincrement=False, nullable=True
- ),
- sa.Column("is_location", sa.Boolean(), autoincrement=False, nullable=True),
- sa.Column("is_fixed", sa.Boolean(), autoincrement=False, nullable=True),
- sa.Column("notes", sa.Text(), autoincrement=False, nullable=True),
- sa.Column(
- "thumbnail_url", sa.String(length=255), autoincrement=False, nullable=True
- ),
- sa.Column(
- "image_url", sa.String(length=255), autoincrement=False, nullable=True
- ),
- sa.Column("archived", sa.Boolean(), 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_asset_version")
- ),
- )
- op.create_index(
- op.f("ix_asset_version_end_transaction_id"),
- "asset_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_version_operation_type"),
- "asset_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_asset_version_pk_transaction_id",
- "asset_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_asset_version_pk_validity",
- "asset_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_version_transaction_id"),
- "asset_version",
- ["transaction_id"],
- unique=False,
- )
-
- # asset_animal
- op.create_table(
- "asset_animal",
- sa.Column("animal_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("birthdate", sa.DateTime(), nullable=True),
- sa.Column("sex", sa.String(length=1), nullable=True),
- sa.Column("is_sterile", sa.Boolean(), nullable=True),
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.ForeignKeyConstraint(
- ["animal_type_uuid"],
- ["animal_type.uuid"],
- name=op.f("fk_asset_animal_animal_type_uuid_animal_type"),
- ),
- sa.ForeignKeyConstraint(
- ["uuid"], ["asset.uuid"], name=op.f("fk_asset_animal_uuid_asset")
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_animal")),
- )
- op.create_table(
- "asset_animal_version",
- sa.Column(
- "animal_type_uuid",
- wuttjamaican.db.util.UUID(),
- autoincrement=False,
- nullable=True,
- ),
- sa.Column("birthdate", sa.DateTime(), autoincrement=False, nullable=True),
- sa.Column("sex", sa.String(length=1), autoincrement=False, nullable=True),
- sa.Column("is_sterile", sa.Boolean(), autoincrement=False, nullable=True),
- 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_animal_version")
- ),
- )
- op.create_index(
- op.f("ix_asset_animal_version_end_transaction_id"),
- "asset_animal_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_animal_version_operation_type"),
- "asset_animal_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_asset_animal_version_pk_transaction_id",
- "asset_animal_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_asset_animal_version_pk_validity",
- "asset_animal_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_animal_version_transaction_id"),
- "asset_animal_version",
- ["transaction_id"],
- unique=False,
- )
-
-
-def downgrade() -> None:
-
- # asset_animal
- op.drop_index(
- op.f("ix_asset_animal_version_transaction_id"),
- table_name="asset_animal_version",
- )
- op.drop_index(
- "ix_asset_animal_version_pk_validity", table_name="asset_animal_version"
- )
- op.drop_index(
- "ix_asset_animal_version_pk_transaction_id", table_name="asset_animal_version"
- )
- op.drop_index(
- op.f("ix_asset_animal_version_operation_type"),
- table_name="asset_animal_version",
- )
- op.drop_index(
- op.f("ix_asset_animal_version_end_transaction_id"),
- table_name="asset_animal_version",
- )
- op.drop_table("asset_animal_version")
- op.drop_table("asset_animal")
-
- # asset
- op.drop_index(op.f("ix_asset_version_transaction_id"), table_name="asset_version")
- op.drop_index("ix_asset_version_pk_validity", table_name="asset_version")
- op.drop_index("ix_asset_version_pk_transaction_id", table_name="asset_version")
- op.drop_index(op.f("ix_asset_version_operation_type"), table_name="asset_version")
- op.drop_index(
- op.f("ix_asset_version_end_transaction_id"), table_name="asset_version"
- )
- op.drop_table("asset_version")
- op.drop_table("asset")
-
- # animal
- op.create_table(
- "animal",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
- sa.Column("animal_type_uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("birthdate", sa.DateTime(), autoincrement=False, nullable=True),
- sa.Column("sex", sa.VARCHAR(length=1), autoincrement=False, nullable=True),
- sa.Column("is_sterile", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column(
- "image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.Column(
- "thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.ForeignKeyConstraint(
- ["animal_type_uuid"],
- ["animal_type.uuid"],
- name=op.f("fk_animal_animal_type_uuid_animal_type"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_animal")),
- sa.UniqueConstraint(
- "drupal_id",
- name=op.f("uq_animal_drupal_id"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "farmos_uuid",
- name=op.f("uq_animal_farmos_uuid"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- )
- op.create_table(
- "animal_version",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
- sa.Column("animal_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column(
- "birthdate", postgresql.TIMESTAMP(), autoincrement=False, nullable=True
- ),
- sa.Column("sex", sa.VARCHAR(length=1), autoincrement=False, nullable=True),
- sa.Column("is_sterile", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column(
- "image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
- sa.Column(
- "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
- ),
- sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
- sa.Column(
- "thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
- ),
- sa.PrimaryKeyConstraint(
- "uuid", "transaction_id", name=op.f("pk_animal_version")
- ),
- )
- op.create_index(
- op.f("ix_animal_version_transaction_id"),
- "animal_version",
- ["transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_animal_version_pk_validity"),
- "animal_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_animal_version_pk_transaction_id"),
- "animal_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- op.f("ix_animal_version_operation_type"),
- "animal_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- op.f("ix_animal_version_end_transaction_id"),
- "animal_version",
- ["end_transaction_id"],
- unique=False,
- )
diff --git a/src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py b/src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py
deleted file mode 100644
index 7c9b5f7..0000000
--- a/src/wuttafarm/db/alembic/versions/d882682c82f9_use_shared_base_for_land_assets.py
+++ /dev/null
@@ -1,411 +0,0 @@
-"""use shared base for Land Assets
-
-Revision ID: d882682c82f9
-Revises: d6e8d16d6854
-Create Date: 2026-02-15 12:00:27.036011
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-import wuttjamaican.db.util
-
-
-# revision identifiers, used by Alembic.
-revision: str = "d882682c82f9"
-down_revision: Union[str, None] = "d6e8d16d6854"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
-
- # asset_parent
- op.create_table(
- "asset_parent",
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("parent_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.ForeignKeyConstraint(
- ["asset_uuid"],
- ["asset.uuid"],
- name=op.f("fk_asset_parent_asset_uuid_asset"),
- ),
- sa.ForeignKeyConstraint(
- ["parent_uuid"],
- ["asset.uuid"],
- name=op.f("fk_asset_parent_parent_uuid_asset"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_parent")),
- )
- op.create_table(
- "asset_parent_version",
- sa.Column(
- "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
- ),
- sa.Column(
- "asset_uuid",
- wuttjamaican.db.util.UUID(),
- autoincrement=False,
- nullable=True,
- ),
- sa.Column(
- "parent_uuid",
- wuttjamaican.db.util.UUID(),
- 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_asset_parent_version")
- ),
- )
- op.create_index(
- op.f("ix_asset_parent_version_end_transaction_id"),
- "asset_parent_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_parent_version_operation_type"),
- "asset_parent_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_asset_parent_version_pk_transaction_id",
- "asset_parent_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_asset_parent_version_pk_validity",
- "asset_parent_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_parent_version_transaction_id"),
- "asset_parent_version",
- ["transaction_id"],
- unique=False,
- )
-
- # asset_land
- op.create_table(
- "asset_land",
- sa.Column("land_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
- sa.ForeignKeyConstraint(
- ["land_type_uuid"],
- ["land_type.uuid"],
- name=op.f("fk_asset_land_land_type_uuid_land_type"),
- ),
- sa.ForeignKeyConstraint(
- ["uuid"], ["asset.uuid"], name=op.f("fk_asset_land_uuid_asset")
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_land")),
- sa.UniqueConstraint(
- "land_type_uuid", name=op.f("uq_asset_land_land_type_uuid")
- ),
- )
- op.create_table(
- "asset_land_version",
- sa.Column(
- "land_type_uuid",
- wuttjamaican.db.util.UUID(),
- autoincrement=False,
- nullable=True,
- ),
- 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_land_version")
- ),
- )
- op.create_index(
- op.f("ix_asset_land_version_end_transaction_id"),
- "asset_land_version",
- ["end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_land_version_operation_type"),
- "asset_land_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- "ix_asset_land_version_pk_transaction_id",
- "asset_land_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- "ix_asset_land_version_pk_validity",
- "asset_land_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_asset_land_version_transaction_id"),
- "asset_land_version",
- ["transaction_id"],
- unique=False,
- )
-
- # land_asset_parent
- op.drop_index(
- op.f("ix_land_asset_parent_version_end_transaction_id"),
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- op.f("ix_land_asset_parent_version_operation_type"),
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- op.f("ix_land_asset_parent_version_pk_transaction_id"),
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- op.f("ix_land_asset_parent_version_pk_validity"),
- table_name="land_asset_parent_version",
- )
- op.drop_index(
- op.f("ix_land_asset_parent_version_transaction_id"),
- table_name="land_asset_parent_version",
- )
- op.drop_table("land_asset_parent_version")
- op.drop_table("land_asset_parent")
-
- # land_asset
- op.drop_index(
- op.f("ix_land_asset_version_end_transaction_id"),
- table_name="land_asset_version",
- )
- op.drop_index(
- op.f("ix_land_asset_version_operation_type"), table_name="land_asset_version"
- )
- op.drop_index(
- op.f("ix_land_asset_version_pk_transaction_id"), table_name="land_asset_version"
- )
- op.drop_index(
- op.f("ix_land_asset_version_pk_validity"), table_name="land_asset_version"
- )
- op.drop_index(
- op.f("ix_land_asset_version_transaction_id"), table_name="land_asset_version"
- )
- op.drop_table("land_asset_version")
- op.drop_table("land_asset")
-
-
-def downgrade() -> None:
-
- # land_asset
- op.create_table(
- "land_asset",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
- sa.Column("land_type_uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.ForeignKeyConstraint(
- ["land_type_uuid"],
- ["land_type.uuid"],
- name=op.f("fk_land_asset_land_type_uuid_land_type"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset")),
- sa.UniqueConstraint(
- "drupal_id",
- name=op.f("uq_land_asset_drupal_id"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "farmos_uuid",
- name=op.f("uq_land_asset_farmos_uuid"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "land_type_uuid",
- name=op.f("uq_land_asset_land_type_uuid"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- sa.UniqueConstraint(
- "name",
- name=op.f("uq_land_asset_name"),
- postgresql_include=[],
- postgresql_nulls_not_distinct=False,
- ),
- )
- op.create_table(
- "land_asset_version",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
- sa.Column("land_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
- sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
- sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
- sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
- sa.Column(
- "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
- ),
- sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint(
- "uuid", "transaction_id", name=op.f("pk_land_asset_version")
- ),
- )
- op.create_index(
- op.f("ix_land_asset_version_transaction_id"),
- "land_asset_version",
- ["transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_version_pk_validity"),
- "land_asset_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_version_pk_transaction_id"),
- "land_asset_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_version_operation_type"),
- "land_asset_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_version_end_transaction_id"),
- "land_asset_version",
- ["end_transaction_id"],
- unique=False,
- )
-
- # land_asset_parent
- op.create_table(
- "land_asset_parent",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("land_asset_uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("parent_asset_uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.ForeignKeyConstraint(
- ["land_asset_uuid"],
- ["land_asset.uuid"],
- name=op.f("fk_land_asset_parent_land_asset_uuid_land_asset"),
- ),
- sa.ForeignKeyConstraint(
- ["parent_asset_uuid"],
- ["land_asset.uuid"],
- name=op.f("fk_land_asset_parent_parent_asset_uuid_land_asset"),
- ),
- sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset_parent")),
- )
- op.create_table(
- "land_asset_parent_version",
- sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
- sa.Column("land_asset_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("parent_asset_uuid", sa.UUID(), autoincrement=False, nullable=True),
- sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
- sa.Column(
- "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
- ),
- sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint(
- "uuid", "transaction_id", name=op.f("pk_land_asset_parent_version")
- ),
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_transaction_id"),
- "land_asset_parent_version",
- ["transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_pk_validity"),
- "land_asset_parent_version",
- ["uuid", "transaction_id", "end_transaction_id"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_pk_transaction_id"),
- "land_asset_parent_version",
- ["uuid", sa.literal_column("transaction_id DESC")],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_operation_type"),
- "land_asset_parent_version",
- ["operation_type"],
- unique=False,
- )
- op.create_index(
- op.f("ix_land_asset_parent_version_end_transaction_id"),
- "land_asset_parent_version",
- ["end_transaction_id"],
- unique=False,
- )
-
- # asset_land
- op.drop_table("asset_land")
- op.drop_index(
- op.f("ix_asset_land_version_transaction_id"), table_name="asset_land_version"
- )
- op.drop_index("ix_asset_land_version_pk_validity", table_name="asset_land_version")
- op.drop_index(
- "ix_asset_land_version_pk_transaction_id", table_name="asset_land_version"
- )
- op.drop_index(
- op.f("ix_asset_land_version_operation_type"), table_name="asset_land_version"
- )
- op.drop_index(
- op.f("ix_asset_land_version_end_transaction_id"),
- table_name="asset_land_version",
- )
- op.drop_table("asset_land_version")
-
- # asset_parent
- op.drop_index(
- op.f("ix_asset_parent_version_transaction_id"),
- table_name="asset_parent_version",
- )
- op.drop_index(
- "ix_asset_parent_version_pk_validity", table_name="asset_parent_version"
- )
- op.drop_index(
- "ix_asset_parent_version_pk_transaction_id", table_name="asset_parent_version"
- )
- op.drop_index(
- op.f("ix_asset_parent_version_operation_type"),
- table_name="asset_parent_version",
- )
- op.drop_index(
- op.f("ix_asset_parent_version_end_transaction_id"),
- table_name="asset_parent_version",
- )
- op.drop_table("asset_parent_version")
- op.drop_table("asset_parent")
diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py
index 367bc1c..27d0070 100644
--- a/src/wuttafarm/db/model/__init__.py
+++ b/src/wuttafarm/db/model/__init__.py
@@ -30,9 +30,9 @@ from wuttjamaican.db.model import *
from .users import WuttaFarmUser
# wuttafarm proper models
-from .assets import AssetType, Asset, AssetParent
+from .assets import AssetType
from .land import LandType, LandAsset
-from .structures import StructureType, StructureAsset
-from .animals import AnimalType, AnimalAsset
-from .groups import GroupAsset
+from .structures import StructureType, Structure
+from .animals import AnimalType, Animal
+from .groups import Group
from .logs import LogType, ActivityLog
diff --git a/src/wuttafarm/db/model/animals.py b/src/wuttafarm/db/model/animals.py
index 548be86..e23f0c5 100644
--- a/src/wuttafarm/db/model/animals.py
+++ b/src/wuttafarm/db/model/animals.py
@@ -28,8 +28,6 @@ from sqlalchemy import orm
from wuttjamaican.db import model
-from wuttafarm.db.model.assets import AssetMixin, add_asset_proxies
-
class AnimalType(model.Base):
"""
@@ -96,19 +94,28 @@ class AnimalType(model.Base):
return self.name or ""
-class AnimalAsset(AssetMixin, model.Base):
+class Animal(model.Base):
"""
- Represents an animal asset from farmOS
+ Represents an animal from farmOS
"""
- __tablename__ = "asset_animal"
+ __tablename__ = "animal"
__versioned__ = {}
__wutta_hint__ = {
- "model_title": "Animal Asset",
- "model_title_plural": "Animal Assets",
- "farmos_asset_type": "animal",
+ "model_title": "Animal",
+ "model_title_plural": "Animals",
}
+ uuid = model.uuid_column()
+
+ name = sa.Column(
+ sa.String(length=100),
+ nullable=False,
+ doc="""
+ Name for the animal.
+ """,
+ )
+
animal_type_uuid = model.uuid_fk_column("animal_type.uuid", nullable=False)
animal_type = orm.relationship(
"AnimalType",
@@ -141,5 +148,47 @@ class AnimalAsset(AssetMixin, model.Base):
""",
)
+ active = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the animal is currently active.
+ """,
+ )
-add_asset_proxies(AnimalAsset)
+ notes = sa.Column(
+ sa.Text(),
+ nullable=True,
+ doc="""
+ Arbitrary notes for the animal.
+ """,
+ )
+
+ image_url = sa.Column(
+ sa.String(length=255),
+ nullable=True,
+ doc="""
+ Optional image URL for the animal.
+ """,
+ )
+
+ farmos_uuid = sa.Column(
+ model.UUID(),
+ nullable=True,
+ unique=True,
+ doc="""
+ UUID for the animal within farmOS.
+ """,
+ )
+
+ drupal_id = sa.Column(
+ sa.Integer(),
+ nullable=True,
+ unique=True,
+ doc="""
+ Drupal internal ID for the animal.
+ """,
+ )
+
+ def __str__(self):
+ return self.name or ""
diff --git a/src/wuttafarm/db/model/assets.py b/src/wuttafarm/db/model/assets.py
index 531fd62..581be62 100644
--- a/src/wuttafarm/db/model/assets.py
+++ b/src/wuttafarm/db/model/assets.py
@@ -25,7 +25,6 @@ Model definition for Asset Types
import sqlalchemy as sa
from sqlalchemy import orm
-from sqlalchemy.ext.declarative import declared_attr
from wuttjamaican.db import model
@@ -81,160 +80,3 @@ class AssetType(model.Base):
def __str__(self):
return self.name or ""
-
-
-class Asset(model.Base):
- """
- Represents an asset (of any kind) from farmOS.
- """
-
- __tablename__ = "asset"
- __versioned__ = {}
- __wutta_hint__ = {
- "model_title": "Asset",
- "model_title_plural": "All Assets",
- }
-
- uuid = model.uuid_column()
-
- farmos_uuid = sa.Column(
- model.UUID(),
- nullable=True,
- unique=True,
- doc="""
- UUID for the asset within farmOS.
- """,
- )
-
- drupal_id = sa.Column(
- sa.Integer(),
- nullable=True,
- unique=True,
- doc="""
- Drupal internal ID for the asset.
- """,
- )
-
- asset_type = sa.Column(
- sa.String(length=100), sa.ForeignKey("asset_type.drupal_id"), nullable=False
- )
-
- asset_name = sa.Column(
- sa.String(length=100),
- nullable=False,
- doc="""
- Name of the asset.
- """,
- )
-
- is_location = sa.Column(
- sa.Boolean(),
- nullable=False,
- default=False,
- doc="""
- Whether the asset should be considered a location.
- """,
- )
-
- is_fixed = sa.Column(
- sa.Boolean(),
- nullable=False,
- default=False,
- doc="""
- Whether the asset's location is fixed.
- """,
- )
-
- notes = sa.Column(
- sa.Text(),
- nullable=True,
- doc="""
- Notes for the asset.
- """,
- )
-
- thumbnail_url = sa.Column(
- sa.String(length=255),
- nullable=True,
- doc="""
- Optional thumbnail URL for the asset.
- """,
- )
-
- image_url = sa.Column(
- sa.String(length=255),
- nullable=True,
- doc="""
- Optional image URL for the asset.
- """,
- )
-
- archived = sa.Column(
- sa.Boolean(),
- nullable=False,
- default=False,
- doc="""
- Whether the asset is archived.
- """,
- )
-
- _parents = orm.relationship(
- "AssetParent",
- foreign_keys="AssetParent.asset_uuid",
- back_populates="asset",
- cascade="all, delete-orphan",
- cascade_backrefs=False,
- )
-
- def __str__(self):
- return self.asset_name or ""
-
-
-class AssetMixin:
-
- uuid = model.uuid_fk_column("asset.uuid", nullable=False, primary_key=True)
-
- @declared_attr
- def asset(cls):
- return orm.relationship(Asset)
-
- def __str__(self):
- return self.asset_name or ""
-
-
-def add_asset_proxies(subclass):
- Asset.make_proxy(subclass, "asset", "farmos_uuid")
- Asset.make_proxy(subclass, "asset", "drupal_id")
- Asset.make_proxy(subclass, "asset", "asset_type")
- Asset.make_proxy(subclass, "asset", "asset_name")
- Asset.make_proxy(subclass, "asset", "is_location")
- Asset.make_proxy(subclass, "asset", "is_fixed")
- Asset.make_proxy(subclass, "asset", "notes")
- Asset.make_proxy(subclass, "asset", "thumbnail_url")
- Asset.make_proxy(subclass, "asset", "image_url")
- Asset.make_proxy(subclass, "asset", "archived")
-
-
-class AssetParent(model.Base):
- """
- Represents an "asset's parent relationship" from farmOS.
- """
-
- __tablename__ = "asset_parent"
- __versioned__ = {}
-
- uuid = model.uuid_column()
-
- asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
-
- asset = orm.relationship(
- Asset,
- foreign_keys=asset_uuid,
- )
-
- parent_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
-
- parent = orm.relationship(
- Asset,
- foreign_keys=parent_uuid,
- )
diff --git a/src/wuttafarm/db/model/groups.py b/src/wuttafarm/db/model/groups.py
index 84453a7..eae034f 100644
--- a/src/wuttafarm/db/model/groups.py
+++ b/src/wuttafarm/db/model/groups.py
@@ -23,23 +23,84 @@
Model definition for Groups
"""
+import sqlalchemy as sa
+from sqlalchemy import orm
+
from wuttjamaican.db import model
-from wuttafarm.db.model.assets import AssetMixin, add_asset_proxies
-
-class GroupAsset(AssetMixin, model.Base):
+class Group(model.Base):
"""
- Represents a group asset from farmOS
+ Represents a "group" from farmOS
"""
- __tablename__ = "asset_group"
+ __tablename__ = "group"
__versioned__ = {}
__wutta_hint__ = {
- "model_title": "Group Asset",
- "model_title_plural": "Group Assets",
- "farmos_asset_type": "group",
+ "model_title": "Group",
+ "model_title_plural": "Groups",
}
+ uuid = model.uuid_column()
-add_asset_proxies(GroupAsset)
+ name = sa.Column(
+ sa.String(length=100),
+ nullable=False,
+ unique=True,
+ doc="""
+ Name for the group.
+ """,
+ )
+
+ is_location = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the group is considered to be a location.
+ """,
+ )
+
+ is_fixed = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the group location is fixed.
+ """,
+ )
+
+ active = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the group is active.
+ """,
+ )
+
+ notes = sa.Column(
+ sa.Text(),
+ nullable=True,
+ doc="""
+ Arbitrary notes for the group.
+ """,
+ )
+
+ farmos_uuid = sa.Column(
+ model.UUID(),
+ nullable=True,
+ unique=True,
+ doc="""
+ UUID for the group within farmOS.
+ """,
+ )
+
+ drupal_id = sa.Column(
+ sa.Integer(),
+ nullable=True,
+ unique=True,
+ doc="""
+ Drupal internal ID for the group.
+ """,
+ )
+
+ def __str__(self):
+ return self.name or ""
diff --git a/src/wuttafarm/db/model/land.py b/src/wuttafarm/db/model/land.py
index 1221c63..53c93cf 100644
--- a/src/wuttafarm/db/model/land.py
+++ b/src/wuttafarm/db/model/land.py
@@ -28,8 +28,6 @@ from sqlalchemy import orm
from wuttjamaican.db import model
-from wuttafarm.db.model.assets import AssetMixin, add_asset_proxies
-
class LandType(model.Base):
"""
@@ -78,21 +76,81 @@ class LandType(model.Base):
return self.name or ""
-class LandAsset(AssetMixin, model.Base):
+class LandAsset(model.Base):
"""
Represents a "land asset" from farmOS
"""
- __tablename__ = "asset_land"
+ __tablename__ = "land_asset"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Land Asset",
"model_title_plural": "Land Assets",
- "farmos_asset_type": "animal",
}
+ uuid = model.uuid_column()
+
+ name = sa.Column(
+ sa.String(length=100),
+ nullable=False,
+ unique=True,
+ doc="""
+ Name of the land asset.
+ """,
+ )
+
land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False, unique=True)
land_type = orm.relationship(LandType, back_populates="land_assets")
+ is_location = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the land asset should be considered a location.
+ """,
+ )
-add_asset_proxies(LandAsset)
+ is_fixed = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the land asset's location is fixed.
+ """,
+ )
+
+ notes = sa.Column(
+ sa.Text(),
+ nullable=True,
+ doc="""
+ Notes for the land asset.
+ """,
+ )
+
+ active = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the land asset is currently active.
+ """,
+ )
+
+ farmos_uuid = sa.Column(
+ model.UUID(),
+ nullable=True,
+ unique=True,
+ doc="""
+ UUID for the land asset within farmOS.
+ """,
+ )
+
+ drupal_id = sa.Column(
+ sa.Integer(),
+ nullable=True,
+ unique=True,
+ doc="""
+ Drupal internal ID for the land asset.
+ """,
+ )
+
+ def __str__(self):
+ return self.name or ""
diff --git a/src/wuttafarm/db/model/structures.py b/src/wuttafarm/db/model/structures.py
index 8c5371c..d9fccdb 100644
--- a/src/wuttafarm/db/model/structures.py
+++ b/src/wuttafarm/db/model/structures.py
@@ -28,8 +28,6 @@ from sqlalchemy import orm
from wuttjamaican.db import model
-from wuttafarm.db.model.assets import AssetMixin, add_asset_proxies
-
class StructureType(model.Base):
"""
@@ -76,19 +74,37 @@ class StructureType(model.Base):
return self.name or ""
-class StructureAsset(AssetMixin, model.Base):
+class Structure(model.Base):
"""
Represents a structure from farmOS
"""
- __tablename__ = "asset_structure"
+ __tablename__ = "structure"
__versioned__ = {}
__wutta_hint__ = {
- "model_title": "Structure Asset",
- "model_title_plural": "Structure Assets",
- "farmos_asset_type": "structure",
+ "model_title": "Structure",
+ "model_title_plural": "Structures",
}
+ uuid = model.uuid_column()
+
+ name = sa.Column(
+ sa.String(length=100),
+ nullable=False,
+ unique=True,
+ doc="""
+ Name for the structure.
+ """,
+ )
+
+ active = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the structure is currently active.
+ """,
+ )
+
structure_type_uuid = model.uuid_fk_column("structure_type.uuid", nullable=False)
structure_type = orm.relationship(
"StructureType",
@@ -97,5 +113,55 @@ class StructureAsset(AssetMixin, model.Base):
""",
)
+ is_location = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the structure is considered a location.
+ """,
+ )
-add_asset_proxies(StructureAsset)
+ is_fixed = sa.Column(
+ sa.Boolean(),
+ nullable=False,
+ doc="""
+ Whether the structure location is fixed.
+ """,
+ )
+
+ notes = sa.Column(
+ sa.Text(),
+ nullable=True,
+ doc="""
+ Arbitrary notes for the structure.
+ """,
+ )
+
+ image_url = sa.Column(
+ sa.String(length=255),
+ nullable=True,
+ doc="""
+ Optional image URL for the structure.
+ """,
+ )
+
+ farmos_uuid = sa.Column(
+ model.UUID(),
+ nullable=True,
+ unique=True,
+ doc="""
+ UUID for the structure within farmOS.
+ """,
+ )
+
+ drupal_id = sa.Column(
+ sa.Integer(),
+ nullable=True,
+ unique=True,
+ doc="""
+ Drupal internal ID for the structure.
+ """,
+ )
+
+ def __str__(self):
+ return self.name or ""
diff --git a/src/wuttafarm/emails.py b/src/wuttafarm/emails.py
index 05416ab..55b1612 100644
--- a/src/wuttafarm/emails.py
+++ b/src/wuttafarm/emails.py
@@ -26,12 +26,6 @@ Email sending config for WuttaFarm
from wuttasync.emails import ImportExportWarning
-class export_to_farmos_from_wuttafarm_warning(ImportExportWarning):
- """
- Diff warning for WuttaFarm → farmOS export.
- """
-
-
class import_to_wuttafarm_from_farmos_warning(ImportExportWarning):
"""
Diff warning for farmOS → WuttaFarm import.
diff --git a/src/wuttafarm/enum.py b/src/wuttafarm/enum.py
deleted file mode 100644
index 41bf597..0000000
--- a/src/wuttafarm/enum.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- 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 .
-#
-################################################################################
-"""
-WuttaFarm enum values
-"""
-
-from collections import OrderedDict
-
-from wuttjamaican.enum import *
-
-
-ANIMAL_SEX = OrderedDict(
- [
- ("M", "Male"),
- ("F", "Female"),
- ]
-)
diff --git a/src/wuttafarm/farmos/importing/__init__.py b/src/wuttafarm/farmos/importing/__init__.py
deleted file mode 100644
index a4b17eb..0000000
--- a/src/wuttafarm/farmos/importing/__init__.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# -*- 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 .
-#
-################################################################################
-"""
-Importing data *into* farmOS
-"""
-
-from . import model
diff --git a/src/wuttafarm/farmos/importing/model.py b/src/wuttafarm/farmos/importing/model.py
deleted file mode 100644
index 6c3f5a0..0000000
--- a/src/wuttafarm/farmos/importing/model.py
+++ /dev/null
@@ -1,365 +0,0 @@
-# -*- 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 .
-#
-################################################################################
-"""
-Importer models targeting farmOS
-"""
-
-import datetime
-from uuid import UUID
-
-import requests
-
-from wuttasync.importing import Importer
-
-
-class ToFarmOS(Importer):
- """
- Base class for data importer targeting the farmOS API.
- """
-
- key = "uuid"
- caches_target = True
-
- def format_datetime(self, dt):
- """
- Convert a WuttaFarm datetime object to the format required for
- pushing to the farmOS API.
- """
- if dt is None:
- return None
- dt = self.app.localtime(dt)
- return dt.timestamp()
-
- def normalize_datetime(self, dt):
- """
- Convert a farmOS datetime value to naive UTC used by
- WuttaFarm.
-
- :param dt: Date/time string value "as-is" from the farmOS API.
-
- :returns: Equivalent naive UTC ``datetime``
- """
- if dt is None:
- return None
- dt = datetime.datetime.fromisoformat(dt)
- return self.app.make_utc(dt)
-
-
-class ToFarmOSAsset(ToFarmOS):
- """
- Base class for asset data importer targeting the farmOS API.
- """
-
- farmos_asset_type = None
-
- def get_target_objects(self, **kwargs):
- assets = self.farmos_client.asset.get(self.farmos_asset_type)
- return assets["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:
- asset = self.farmos_client.asset.get_id(self.farmos_asset_type, str(uuid))
- except requests.HTTPError as exc:
- if exc.response.status_code == 404:
- return None
- raise
- return asset["data"]
-
- 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_asset_payload(source_data)
- result = self.farmos_client.asset.send(self.farmos_asset_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_asset_payload(source_data)
- payload["id"] = str(source_data["uuid"])
- result = self.farmos_client.asset.send(self.farmos_asset_type, payload)
- return self.normalize_target_object(result["data"])
-
- def normalize_target_object(self, asset):
-
- if notes := asset["attributes"]["notes"]:
- notes = notes["value"]
-
- return {
- "uuid": UUID(asset["id"]),
- "asset_name": asset["attributes"]["name"],
- "is_location": asset["attributes"]["is_location"],
- "is_fixed": asset["attributes"]["is_fixed"],
- "notes": notes,
- "archived": asset["attributes"]["archived"],
- }
-
- def get_asset_payload(self, source_data):
-
- attrs = {}
- if "asset_name" in self.fields:
- attrs["name"] = source_data["asset_name"]
- if "is_location" in self.fields:
- attrs["is_location"] = source_data["is_location"]
- if "is_fixed" in self.fields:
- attrs["is_fixed"] = source_data["is_fixed"]
- if "notes" in self.fields:
- attrs["notes"] = {"value": source_data["notes"]}
- if "archived" in self.fields:
- attrs["archived"] = source_data["archived"]
-
- payload = {"attributes": attrs}
-
- return payload
-
-
-class AnimalAssetImporter(ToFarmOSAsset):
-
- model_title = "AnimalAsset"
- farmos_asset_type = "animal"
-
- supported_fields = [
- "uuid",
- "asset_name",
- "animal_type_uuid",
- "sex",
- "is_sterile",
- "birthdate",
- "notes",
- "archived",
- ]
-
- def normalize_target_object(self, animal):
- data = super().normalize_target_object(animal)
- data.update(
- {
- "animal_type_uuid": UUID(
- animal["relationships"]["animal_type"]["data"]["id"]
- ),
- "sex": animal["attributes"]["sex"],
- "is_sterile": animal["attributes"]["is_sterile"],
- "birthdate": self.normalize_datetime(animal["attributes"]["birthdate"]),
- }
- )
- return data
-
- def get_asset_payload(self, source_data):
- payload = super().get_asset_payload(source_data)
-
- attrs = {}
- if "sex" in self.fields:
- attrs["sex"] = source_data["sex"]
- if "is_sterile" in self.fields:
- attrs["is_sterile"] = source_data["is_sterile"]
- if "birthdate" in self.fields:
- attrs["birthdate"] = self.format_datetime(source_data["birthdate"])
-
- rels = {}
- if "animal_type_uuid" in self.fields:
- rels["animal_type"] = {
- "data": {
- "id": str(source_data["animal_type_uuid"]),
- "type": "taxonomy_term--animal_type",
- }
- }
-
- payload["attributes"].update(attrs)
- if rels:
- payload.setdefault("relationships", {}).update(rels)
-
- return payload
-
-
-class AnimalTypeImporter(ToFarmOS):
-
- 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"])
-
-
-class GroupAssetImporter(ToFarmOSAsset):
-
- model_title = "GroupAsset"
- farmos_asset_type = "group"
-
- supported_fields = [
- "uuid",
- "asset_name",
- "notes",
- "archived",
- ]
-
-
-class LandAssetImporter(ToFarmOSAsset):
-
- model_title = "LandAsset"
- farmos_asset_type = "land"
-
- supported_fields = [
- "uuid",
- "asset_name",
- "land_type_id",
- "is_location",
- "is_fixed",
- "notes",
- "archived",
- ]
-
- def normalize_target_object(self, land):
- data = super().normalize_target_object(land)
- data.update(
- {
- "land_type_id": land["attributes"]["land_type"],
- }
- )
- return data
-
- def get_asset_payload(self, source_data):
- payload = super().get_asset_payload(source_data)
-
- attrs = {}
- if "land_type_id" in self.fields:
- attrs["land_type"] = source_data["land_type_id"]
-
- if attrs:
- payload["attributes"].update(attrs)
-
- return payload
-
-
-class StructureAssetImporter(ToFarmOSAsset):
-
- model_title = "StructureAsset"
- farmos_asset_type = "structure"
-
- supported_fields = [
- "uuid",
- "asset_name",
- "structure_type_id",
- "is_location",
- "is_fixed",
- "notes",
- "archived",
- ]
-
- def normalize_target_object(self, structure):
- data = super().normalize_target_object(structure)
- data.update(
- {
- "structure_type_id": structure["attributes"]["structure_type"],
- }
- )
- return data
-
- def get_asset_payload(self, source_data):
- payload = super().get_asset_payload(source_data)
-
- attrs = {}
- if "structure_type_id" in self.fields:
- attrs["structure_type"] = source_data["structure_type_id"]
-
- if attrs:
- payload["attributes"].update(attrs)
-
- return payload
diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py
deleted file mode 100644
index 8ef8a77..0000000
--- a/src/wuttafarm/farmos/importing/wuttafarm.py
+++ /dev/null
@@ -1,263 +0,0 @@
-# -*- 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 .
-#
-################################################################################
-"""
-WuttaFarm → farmOS data export
-"""
-
-from oauthlib.oauth2 import BackendApplicationClient
-from requests_oauthlib import OAuth2Session
-
-from wuttasync.importing import ImportHandler, FromWuttaHandler, FromWutta, Orientation
-
-from wuttafarm.db import model
-from wuttafarm.farmos import importing as farmos_importing
-
-
-class FromWuttaFarmHandler(FromWuttaHandler):
- """
- Base class for import handler targeting WuttaFarm
- """
-
- source_key = "wuttafarm"
-
-
-class ToFarmOSHandler(ImportHandler):
- """
- Base class for export handlers using CSV file(s) as data target.
- """
-
- target_key = "farmos"
- generic_target_title = "farmOS"
-
- # TODO: a lot of duplication to cleanup here; see FromFarmOSHandler
-
- def begin_target_transaction(self):
- """
- Establish the farmOS API client.
- """
- token = self.get_farmos_oauth2_token()
- self.farmos_client = self.app.get_farmos_client(token=token)
- self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
-
- def get_farmos_oauth2_token(self):
-
- client_id = self.config.get(
- "farmos.oauth2.importing.client_id", default="wuttafarm"
- )
- client_secret = self.config.require("farmos.oauth2.importing.client_secret")
- scope = self.config.get("farmos.oauth2.importing.scope", default="farm_manager")
-
- client = BackendApplicationClient(client_id=client_id)
- oauth = OAuth2Session(client=client)
-
- return oauth.fetch_token(
- token_url=self.app.get_farmos_url("/oauth/token"),
- include_client_id=True,
- client_secret=client_secret,
- scope=scope,
- )
-
- def get_importer_kwargs(self, key, **kwargs):
- kwargs = super().get_importer_kwargs(key, **kwargs)
- kwargs["farmos_client"] = self.farmos_client
- kwargs["farmos_4x"] = self.farmos_4x
- return kwargs
-
-
-class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
- """
- Handler for WuttaFarm → farmOS API export.
- """
-
- orientation = Orientation.EXPORT
-
- def define_importers(self):
- """ """
- importers = super().define_importers()
- importers["LandAsset"] = LandAssetImporter
- importers["StructureAsset"] = StructureAssetImporter
- importers["AnimalType"] = AnimalTypeImporter
- importers["AnimalAsset"] = AnimalAssetImporter
- importers["GroupAsset"] = GroupAssetImporter
- return importers
-
-
-class FromWuttaFarm(FromWutta):
-
- drupal_internal_id_field = "drupal_internal__id"
-
- def create_target_object(self, key, source_data):
- obj = super().create_target_object(key, source_data)
- if obj is None:
- return None
-
- if not self.dry_run:
-
- # set farmOS, Drupal key fields in WuttaFarm
- api_object = obj["_new_object"]
- wf_object = source_data["_src_object"]
- wf_object.farmos_uuid = obj["uuid"]
- wf_object.drupal_id = api_object["attributes"][
- self.drupal_internal_id_field
- ]
-
- return obj
-
-
-class AnimalAssetImporter(FromWuttaFarm, farmos_importing.model.AnimalAssetImporter):
- """
- WuttaFarm → farmOS API exporter for Animal Assets
- """
-
- source_model_class = model.AnimalAsset
-
- supported_fields = [
- "uuid",
- "asset_name",
- "animal_type_uuid",
- "sex",
- "is_sterile",
- "birthdate",
- "notes",
- "archived",
- ]
-
- def normalize_source_object(self, animal):
- return {
- "uuid": animal.farmos_uuid or self.app.make_true_uuid(),
- "asset_name": animal.asset_name,
- "animal_type_uuid": animal.animal_type.farmos_uuid,
- "sex": animal.sex,
- "is_sterile": animal.is_sterile,
- "birthdate": animal.birthdate,
- "notes": animal.notes,
- "archived": animal.archived,
- "_src_object": animal,
- }
-
-
-class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporter):
- """
- WuttaFarm → farmOS API exporter for Animal Types
- """
-
- source_model_class = model.AnimalType
-
- supported_fields = [
- "uuid",
- "name",
- ]
-
- drupal_internal_id_field = "drupal_internal__tid"
-
- def normalize_source_object(self, animal_type):
- return {
- "uuid": animal_type.farmos_uuid or self.app.make_true_uuid(),
- "name": animal_type.name,
- "_src_object": animal_type,
- }
-
-
-class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter):
- """
- WuttaFarm → farmOS API exporter for Group Assets
- """
-
- source_model_class = model.GroupAsset
-
- supported_fields = [
- "uuid",
- "asset_name",
- "notes",
- "archived",
- ]
-
- def normalize_source_object(self, group):
- return {
- "uuid": group.farmos_uuid or self.app.make_true_uuid(),
- "asset_name": group.asset_name,
- "notes": group.notes,
- "archived": group.archived,
- "_src_object": group,
- }
-
-
-class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter):
- """
- WuttaFarm → farmOS API exporter for Land Assets
- """
-
- source_model_class = model.LandAsset
-
- supported_fields = [
- "uuid",
- "asset_name",
- "land_type_id",
- "is_location",
- "is_fixed",
- "notes",
- "archived",
- ]
-
- def normalize_source_object(self, land):
- return {
- "uuid": land.farmos_uuid or self.app.make_true_uuid(),
- "asset_name": land.asset_name,
- "land_type_id": land.land_type.drupal_id,
- "is_location": land.is_location,
- "is_fixed": land.is_fixed,
- "notes": land.notes,
- "archived": land.archived,
- "_src_object": land,
- }
-
-
-class StructureAssetImporter(
- FromWuttaFarm, farmos_importing.model.StructureAssetImporter
-):
- """
- WuttaFarm → farmOS API exporter for Structure Assets
- """
-
- source_model_class = model.StructureAsset
-
- supported_fields = [
- "uuid",
- "asset_name",
- "structure_type_id",
- "is_location",
- "is_fixed",
- "notes",
- "archived",
- ]
-
- def normalize_source_object(self, structure):
- return {
- "uuid": structure.farmos_uuid or self.app.make_true_uuid(),
- "asset_name": structure.asset_name,
- "structure_type_id": structure.structure_type.drupal_id,
- "is_location": structure.is_location,
- "is_fixed": structure.is_fixed,
- "notes": structure.notes,
- "archived": structure.archived,
- "_src_object": structure,
- }
diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py
index 4f9db20..4acbe24 100644
--- a/src/wuttafarm/importing/farmos.py
+++ b/src/wuttafarm/importing/farmos.py
@@ -100,10 +100,10 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
importers["LandType"] = LandTypeImporter
importers["LandAsset"] = LandAssetImporter
importers["StructureType"] = StructureTypeImporter
- importers["StructureAsset"] = StructureAssetImporter
+ importers["Structure"] = StructureImporter
importers["AnimalType"] = AnimalTypeImporter
- importers["AnimalAsset"] = AnimalAssetImporter
- importers["GroupAsset"] = GroupAssetImporter
+ importers["Animal"] = AnimalImporter
+ importers["Group"] = GroupImporter
importers["LogType"] = LogTypeImporter
importers["ActivityLog"] = ActivityLogImporter
return importers
@@ -176,170 +176,24 @@ class ActivityLogImporter(FromFarmOS, ToWutta):
}
-class AssetImporterBase(FromFarmOS, ToWutta):
- """
- Base class for farmOS API → WuttaFarm asset importers
- """
-
- def get_simple_fields(self):
- """ """
- fields = list(super().get_simple_fields())
- # nb. must explicitly declare proxy fields
- fields.extend(
- [
- "farmos_uuid",
- "drupal_id",
- "asset_type",
- "asset_name",
- "is_location",
- "is_fixed",
- "notes",
- "archived",
- "image_url",
- "thumbnail_url",
- ]
- )
- return fields
-
- def get_supported_fields(self):
- """ """
- fields = list(super().get_supported_fields())
- fields.extend(
- [
- "parents",
- ]
- )
- return fields
-
- def normalize_source_data(self, **kwargs):
- """ """
- data = super().normalize_source_data(**kwargs)
-
- if "parents" in self.fields:
- # nb. make sure parent-less (root) assets come first, so they
- # exist when child assets need to reference them
- data.sort(key=lambda l: len(l["parents"]))
-
- return data
-
- def normalize_asset(self, asset):
- """ """
- image_url = None
- thumbnail_url = None
- if relationships := asset.get("relationships"):
-
- if image := relationships.get("image"):
- if image["data"]:
- image = self.farmos_client.resource.get_id(
- "file", "file", image["data"][0]["id"]
- )
- if image_style := image["data"]["attributes"].get(
- "image_style_uri"
- ):
- image_url = image_style["large"]
- thumbnail_url = image_style["thumbnail"]
-
- if notes := asset["attributes"]["notes"]:
- notes = notes["value"]
-
- if self.farmos_4x:
- archived = asset["attributes"]["archived"]
- else:
- archived = asset["attributes"]["status"] == "archived"
-
- parents = None
- if "parents" in self.fields:
- parents = []
- for parent in asset["relationships"]["parent"]["data"]:
- parents.append((self.get_asset_type(parent), UUID(parent["id"])))
-
- return {
- "farmos_uuid": UUID(asset["id"]),
- "drupal_id": asset["attributes"]["drupal_internal__id"],
- "asset_name": asset["attributes"]["name"],
- "is_location": asset["attributes"]["is_location"],
- "is_fixed": asset["attributes"]["is_fixed"],
- "archived": archived,
- "notes": notes,
- "image_url": image_url,
- "thumbnail_url": thumbnail_url,
- "parents": parents,
- }
-
- def get_asset_type(self, asset):
- return asset["type"].split("--")[1]
-
- def normalize_target_object(self, asset):
- data = super().normalize_target_object(asset)
-
- if "parents" in self.fields:
- data["parents"] = [
- (p.parent.asset_type, p.parent.farmos_uuid)
- for p in asset.asset._parents
- ]
-
- return data
-
- def update_target_object(self, asset, source_data, target_data=None):
- model = self.app.model
- asset = super().update_target_object(asset, source_data, target_data)
-
- if "parents" in self.fields:
- if not target_data or target_data["parents"] != source_data["parents"]:
-
- for key in source_data["parents"]:
- asset_type, farmos_uuid = key
- if not target_data or key not in target_data["parents"]:
- self.target_session.flush()
- parent = (
- self.target_session.query(model.Asset)
- .filter(model.Asset.asset_type == asset_type)
- .filter(model.Asset.farmos_uuid == farmos_uuid)
- .one()
- )
- asset.asset._parents.append(model.AssetParent(parent=parent))
-
- if target_data:
- for key in target_data["parents"]:
- asset_type, farmos_uuid = key
- if key not in source_data["parents"]:
- parent = (
- self.target_session.query(model.Asset)
- .filter(model.Asset.asset_type == asset_type)
- .filter(model.Asset.farmos_uuid == farmos_uuid)
- .one()
- )
- parent = (
- self.target_session.query(model.AssetParent)
- .filter(model.AssetParent.asset == asset)
- .filter(model.AssetParent.parent == parent)
- .one()
- )
- self.target_session.delete(parent)
-
- return asset
-
-
-class AnimalAssetImporter(AssetImporterBase):
+class AnimalImporter(FromFarmOS, ToWutta):
"""
farmOS API → WuttaFarm importer for Animals
"""
- model_class = model.AnimalAsset
+ model_class = model.Animal
supported_fields = [
"farmos_uuid",
"drupal_id",
- "asset_type",
- "asset_name",
+ "name",
"animal_type_uuid",
"sex",
"is_sterile",
"birthdate",
"notes",
- "archived",
+ "active",
"image_url",
- "thumbnail_url",
]
def setup(self):
@@ -359,18 +213,25 @@ class AnimalAssetImporter(AssetImporterBase):
def normalize_source_object(self, animal):
""" """
animal_type_uuid = None
+ image_url = None
if relationships := animal.get("relationships"):
if animal_type := relationships.get("animal_type"):
if animal_type["data"]:
- if wf_animal_type := self.animal_types_by_farmos_uuid.get(
+ if animal_type := self.animal_types_by_farmos_uuid.get(
UUID(animal_type["data"]["id"])
):
- animal_type_uuid = wf_animal_type.uuid
- else:
- log.warning(
- "animal type not found: %s", animal_type["data"]["id"]
- )
+ animal_type_uuid = animal_type.uuid
+
+ if image := relationships.get("image"):
+ if image["data"]:
+ image = self.farmos_client.resource.get_id(
+ "file", "file", image["data"][0]["id"]
+ )
+ if image_style := image["data"]["attributes"].get(
+ "image_style_uri"
+ ):
+ image_url = image_style["large"]
if not animal_type_uuid:
log.warning("missing/invalid animal_type for farmOS Animal: %s", animal)
@@ -387,17 +248,26 @@ class AnimalAssetImporter(AssetImporterBase):
else:
sterile = animal["attributes"]["is_castrated"]
- data = self.normalize_asset(animal)
- data.update(
- {
- "asset_type": "animal",
- "animal_type_uuid": animal_type_uuid,
- "sex": animal["attributes"]["sex"],
- "is_sterile": sterile,
- "birthdate": birthdate,
- }
- )
- return data
+ if notes := animal["attributes"]["notes"]:
+ notes = notes["value"]
+
+ if self.farmos_4x:
+ active = not animal["attributes"]["archived"]
+ else:
+ active = animal["attributes"]["status"] == "active"
+
+ return {
+ "farmos_uuid": UUID(animal["id"]),
+ "drupal_id": animal["attributes"]["drupal_internal__id"],
+ "name": animal["attributes"]["name"],
+ "animal_type_uuid": animal_type.uuid,
+ "sex": animal["attributes"]["sex"],
+ "is_sterile": sterile,
+ "birthdate": birthdate,
+ "active": active,
+ "notes": notes,
+ "image_url": image_url,
+ }
class AnimalTypeImporter(FromFarmOS, ToWutta):
@@ -460,25 +330,21 @@ class AssetTypeImporter(FromFarmOS, ToWutta):
}
-class GroupAssetImporter(AssetImporterBase):
+class GroupImporter(FromFarmOS, ToWutta):
"""
- farmOS API → WuttaFarm importer for Group Assets
+ farmOS API → WuttaFarm importer for Groups
"""
- model_class = model.GroupAsset
+ model_class = model.Group
supported_fields = [
"farmos_uuid",
"drupal_id",
- "asset_type",
- "asset_name",
+ "name",
"is_location",
"is_fixed",
"notes",
- "archived",
- "image_url",
- "thumbnail_url",
- "parents",
+ "active",
]
def get_source_objects(self):
@@ -488,16 +354,26 @@ class GroupAssetImporter(AssetImporterBase):
def normalize_source_object(self, group):
""" """
- data = self.normalize_asset(group)
- data.update(
- {
- "asset_type": "group",
- }
- )
- return data
+ if notes := group["attributes"]["notes"]:
+ notes = notes["value"]
+
+ if self.farmos_4x:
+ active = not group["attributes"]["archived"]
+ else:
+ active = group["attributes"]["status"] == "active"
+
+ return {
+ "farmos_uuid": UUID(group["id"]),
+ "drupal_id": group["attributes"]["drupal_internal__id"],
+ "name": group["attributes"]["name"],
+ "is_location": group["attributes"]["is_location"],
+ "is_fixed": group["attributes"]["is_fixed"],
+ "active": active,
+ "notes": notes,
+ }
-class LandAssetImporter(AssetImporterBase):
+class LandAssetImporter(FromFarmOS, ToWutta):
"""
farmOS API → WuttaFarm importer for Land Assets
"""
@@ -507,18 +383,15 @@ class LandAssetImporter(AssetImporterBase):
supported_fields = [
"farmos_uuid",
"drupal_id",
- "asset_type",
- "asset_name",
+ "name",
"land_type_uuid",
"is_location",
"is_fixed",
"notes",
- "archived",
- "parents",
+ "active",
]
def setup(self):
- """ """
super().setup()
model = self.app.model
@@ -541,14 +414,24 @@ class LandAssetImporter(AssetImporterBase):
)
return None
- data = self.normalize_asset(land)
- data.update(
- {
- "asset_type": "land",
- "land_type_uuid": land_type.uuid,
- }
- )
- return data
+ if notes := land["attributes"]["notes"]:
+ notes = notes["value"]
+
+ if self.farmos_4x:
+ active = not land["attributes"]["archived"]
+ else:
+ active = land["attributes"]["status"] == "active"
+
+ return {
+ "farmos_uuid": UUID(land["id"]),
+ "drupal_id": land["attributes"]["drupal_internal__id"],
+ "name": land["attributes"]["name"],
+ "land_type_uuid": land_type.uuid,
+ "is_location": land["attributes"]["is_location"],
+ "is_fixed": land["attributes"]["is_fixed"],
+ "active": active,
+ "notes": notes,
+ }
class LandTypeImporter(FromFarmOS, ToWutta):
@@ -607,26 +490,23 @@ class LogTypeImporter(FromFarmOS, ToWutta):
}
-class StructureAssetImporter(AssetImporterBase):
+class StructureImporter(FromFarmOS, ToWutta):
"""
- farmOS API → WuttaFarm importer for Structure Assets
+ farmOS API → WuttaFarm importer for Structures
"""
- model_class = model.StructureAsset
+ model_class = model.Structure
supported_fields = [
"farmos_uuid",
"drupal_id",
- "asset_type",
- "asset_name",
+ "name",
"structure_type_uuid",
"is_location",
"is_fixed",
"notes",
- "archived",
+ "active",
"image_url",
- "thumbnail_url",
- "parents",
]
def setup(self):
@@ -648,20 +528,43 @@ class StructureAssetImporter(AssetImporterBase):
structure_type = self.structure_types_by_id.get(structure_type_id)
if not structure_type:
log.warning(
- "invalid structure_type '%s' for farmOS Structure Asset: %s",
+ "invalid structure_type '%s' for farmOS Structure: %s",
structure_type_id,
structure,
)
return None
- data = self.normalize_asset(structure)
- data.update(
- {
- "asset_type": "structure",
- "structure_type_uuid": structure_type.uuid,
- }
- )
- return data
+ if notes := structure["attributes"]["notes"]:
+ notes = notes["value"]
+
+ image_url = None
+ if relationships := structure.get("relationships"):
+ if image := relationships.get("image"):
+ if image["data"]:
+ image = self.farmos_client.resource.get_id(
+ "file", "file", image["data"][0]["id"]
+ )
+ if image_style := image["data"]["attributes"].get(
+ "image_style_uri"
+ ):
+ image_url = image_style["large"]
+
+ if self.farmos_4x:
+ active = not structure["attributes"]["archived"]
+ else:
+ active = structure["attributes"]["status"] == "active"
+
+ return {
+ "farmos_uuid": UUID(structure["id"]),
+ "drupal_id": structure["attributes"]["drupal_internal__id"],
+ "name": structure["attributes"]["name"],
+ "structure_type_uuid": structure_type.uuid,
+ "is_location": structure["attributes"]["is_location"],
+ "is_fixed": structure["attributes"]["is_fixed"],
+ "active": active,
+ "notes": notes,
+ "image_url": image_url,
+ }
class StructureTypeImporter(FromFarmOS, ToWutta):
diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py
index 95b3e9d..f646a96 100644
--- a/src/wuttafarm/web/forms/schema.py
+++ b/src/wuttafarm/web/forms/schema.py
@@ -27,7 +27,7 @@ import json
import colander
-from wuttaweb.forms.schema import ObjectRef, WuttaSet
+from wuttaweb.forms.schema import ObjectRef
class AnimalTypeRef(ObjectRef):
@@ -160,20 +160,3 @@ class UsersType(colander.SchemaType):
from wuttafarm.web.forms.widgets import UsersWidget
return UsersWidget(self.request, **kwargs)
-
-
-class AssetParentRefs(WuttaSet):
- """
- Schema type for Parents field which references assets.
- """
-
- def serialize(self, node, appstruct):
- if not appstruct:
- appstruct = []
- uuids = [u.hex for u in appstruct]
- return json.dumps(uuids)
-
- def widget_maker(self, **kwargs):
- from wuttafarm.web.forms.widgets import AssetParentRefsWidget
-
- return AssetParentRefsWidget(self.request, **kwargs)
diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py
index f812ccf..f6a99fc 100644
--- a/src/wuttafarm/web/forms/widgets.py
+++ b/src/wuttafarm/web/forms/widgets.py
@@ -29,9 +29,6 @@ import colander
from deform.widget import Widget
from webhelpers2.html import HTML, tags
-from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
-from wuttaweb.db import Session
-
class ImageWidget(Widget):
"""
@@ -135,34 +132,3 @@ class UsersWidget(Widget):
return HTML.tag("ul", c=items)
return super().serialize(field, cstruct, **kw)
-
-
-class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
- """
- Widget for Parents field which references assets.
- """
-
- def serialize(self, field, cstruct, **kw):
- """ """
- model = self.app.model
- session = Session()
-
- readonly = kw.get("readonly", self.readonly)
- if readonly:
- parents = []
- for uuid in json.loads(cstruct):
- parent = session.get(model.Asset, uuid)
- parents.append(
- HTML.tag(
- "li",
- c=tags.link_to(
- str(parent),
- self.request.route_url(
- f"{parent.asset_type}_assets.view", uuid=parent.uuid
- ),
- ),
- )
- )
- return HTML.tag("ul", c=parents)
-
- return super().serialize(field, cstruct, **kw)
diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py
index bdd2fbf..3e5bb46 100644
--- a/src/wuttafarm/web/menus.py
+++ b/src/wuttafarm/web/menus.py
@@ -45,46 +45,41 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"type": "menu",
"items": [
{
- "title": "All Assets",
- "route": "assets",
- "perm": "assets.list",
+ "title": "Animals",
+ "route": "animals",
+ "perm": "animals.list",
},
{
- "title": "Animal",
- "route": "animal_assets",
- "perm": "animal_assets.list",
+ "title": "Groups",
+ "route": "groups",
+ "perm": "groups.list",
},
{
- "title": "Group",
- "route": "group_assets",
- "perm": "group_assets.list",
+ "title": "Structures",
+ "route": "structures",
+ "perm": "structures.list",
},
{
"title": "Land",
"route": "land_assets",
"perm": "land_assets.list",
},
- {
- "title": "Structure",
- "route": "structure_assets",
- "perm": "structure_assets.list",
- },
{"type": "sep"},
{
"title": "Animal Types",
"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",
diff --git a/src/wuttafarm/web/templates/assets/master/view.mako b/src/wuttafarm/web/templates/assets/master/view.mako
deleted file mode 100644
index dac5a1c..0000000
--- a/src/wuttafarm/web/templates/assets/master/view.mako
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8; -*-
-<%inherit file="/master/view.mako" />
-
-<%def name="page_content()">
-
- % if instance.archived:
-
- This asset is archived.
- Archived assets should only be edited if they need corrections.
-
- % endif
-
- ${parent.page_content()}
-%def>
diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py
index e44c16e..a4d12dd 100644
--- a/src/wuttafarm/web/views/__init__.py
+++ b/src/wuttafarm/web/views/__init__.py
@@ -42,8 +42,10 @@ def includeme(config):
# native table views
config.include("wuttafarm.web.views.asset_types")
- config.include("wuttafarm.web.views.assets")
- config.include("wuttafarm.web.views.land")
+ 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.land_assets")
config.include("wuttafarm.web.views.structures")
config.include("wuttafarm.web.views.animals")
config.include("wuttafarm.web.views.groups")
diff --git a/src/wuttafarm/web/views/animal_types.py b/src/wuttafarm/web/views/animal_types.py
new file mode 100644
index 0000000..09d1e25
--- /dev/null
+++ b/src/wuttafarm/web/views/animal_types.py
@@ -0,0 +1,128 @@
+# -*- 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 Animal Types
+"""
+
+from wuttafarm.db.model.animals import AnimalType, Animal
+from wuttafarm.web.views import WuttaFarmMasterView
+
+
+class AnimalTypeView(WuttaFarmMasterView):
+ """
+ Master view for Animal Types
+ """
+
+ model_class = AnimalType
+ route_prefix = "animal_types"
+ url_prefix = "/animal-types"
+
+ farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
+
+ grid_columns = [
+ "name",
+ "description",
+ "changed",
+ ]
+
+ sort_defaults = "name"
+
+ filter_defaults = {
+ "name": {"active": True, "verb": "contains"},
+ }
+
+ form_fields = [
+ "name",
+ "description",
+ "changed",
+ "farmos_uuid",
+ "drupal_id",
+ ]
+
+ has_rows = True
+ row_model_class = Animal
+ rows_viewable = True
+
+ row_grid_columns = [
+ "name",
+ "sex",
+ "is_sterile",
+ "birthdate",
+ "active",
+ ]
+
+ rows_sort_defaults = "name"
+
+ def configure_grid(self, grid):
+ g = grid
+ super().configure_grid(g)
+
+ # name
+ g.set_link("name")
+
+ def get_farmos_url(self, animal_type):
+ return self.app.get_farmos_url(f"/taxonomy/term/{animal_type.drupal_id}")
+
+ def get_xref_buttons(self, animal_type):
+ buttons = super().get_xref_buttons(animal_type)
+
+ if animal_type.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_animal_types.view", uuid=animal_type.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
+ def get_row_grid_data(self, animal_type):
+ model = self.app.model
+ session = self.Session()
+ return session.query(model.Animal).filter(
+ model.Animal.animal_type == animal_type
+ )
+
+ def configure_row_grid(self, grid):
+ g = grid
+ super().configure_row_grid(g)
+
+ # name
+ g.set_link("name")
+
+ def get_row_action_url_view(self, animal, i):
+ return self.request.route_url("animals.view", uuid=animal.uuid)
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ AnimalTypeView = kwargs.get("AnimalTypeView", base["AnimalTypeView"])
+ AnimalTypeView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)
diff --git a/src/wuttafarm/web/views/animals.py b/src/wuttafarm/web/views/animals.py
index bae7dde..e22095e 100644
--- a/src/wuttafarm/web/views/animals.py
+++ b/src/wuttafarm/web/views/animals.py
@@ -23,29 +23,30 @@
Master view for Animals
"""
-from wuttaweb.forms.schema import WuttaDictEnum
-
-from wuttafarm.db.model import AnimalType, AnimalAsset
-from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
+from wuttafarm.db.model.animals import Animal
+from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.web.forms.schema import AnimalTypeRef
from wuttafarm.web.forms.widgets import ImageWidget
-class AnimalTypeView(AssetTypeMasterView):
+class AnimalView(WuttaFarmMasterView):
"""
- Master view for Animal Types
+ Master view for Animals
"""
- model_class = AnimalType
- route_prefix = "animal_types"
- url_prefix = "/animal-types"
+ model_class = Animal
+ route_prefix = "animals"
+ url_prefix = "/animals"
- farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
+ farmos_refurl_path = "/assets/animal"
grid_columns = [
"name",
- "description",
- "changed",
+ "animal_type",
+ "sex",
+ "is_sterile",
+ "birthdate",
+ "active",
]
sort_defaults = "name"
@@ -56,126 +57,15 @@ class AnimalTypeView(AssetTypeMasterView):
form_fields = [
"name",
- "description",
- "changed",
- "farmos_uuid",
- "drupal_id",
- ]
-
- has_rows = True
- row_model_class = AnimalAsset
- rows_viewable = True
-
- row_grid_columns = [
- "asset_name",
- "sex",
- "is_sterile",
- "birthdate",
- "archived",
- ]
-
- rows_sort_defaults = "asset_name"
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
-
- # name
- g.set_link("name")
-
- def get_farmos_url(self, animal_type):
- return self.app.get_farmos_url(f"/taxonomy/term/{animal_type.drupal_id}")
-
- def get_xref_buttons(self, animal_type):
- buttons = super().get_xref_buttons(animal_type)
-
- if animal_type.farmos_uuid:
- buttons.append(
- self.make_button(
- "View farmOS record",
- primary=True,
- url=self.request.route_url(
- "farmos_animal_types.view", uuid=animal_type.farmos_uuid
- ),
- icon_left="eye",
- )
- )
-
- return buttons
-
- def get_row_grid_data(self, animal_type):
- model = self.app.model
- session = self.Session()
- return (
- session.query(model.AnimalAsset)
- .join(model.Asset)
- .filter(model.AnimalAsset.animal_type == animal_type)
- )
-
- def configure_row_grid(self, grid):
- g = grid
- super().configure_row_grid(g)
- model = self.app.model
- enum = self.app.enum
-
- # asset_name
- g.set_link("asset_name")
- g.set_sorter("asset_name", model.Asset.asset_name)
- g.set_filter("asset_name", model.Asset.asset_name)
-
- # sex
- g.set_enum("sex", enum.ANIMAL_SEX)
-
- # archived
- g.set_renderer("archived", "boolean")
- g.set_sorter("archived", model.Asset.archived)
- g.set_filter("archived", model.Asset.archived)
-
- def get_row_action_url_view(self, animal, i):
- return self.request.route_url("animal_assets.view", uuid=animal.uuid)
-
-
-class AnimalAssetView(AssetMasterView):
- """
- Master view for Animal Assets
- """
-
- model_class = AnimalAsset
- route_prefix = "animal_assets"
- url_prefix = "/assets/animal"
-
- farmos_refurl_path = "/assets/animal"
-
- labels = {
- "animal_type": "Species/Breed",
- "is_sterile": "Sterile",
- }
-
- grid_columns = [
- "thumbnail",
- "drupal_id",
- "asset_name",
- "animal_type",
- "birthdate",
- "is_sterile",
- "sex",
- "archived",
- ]
-
- form_fields = [
- "asset_name",
"animal_type",
"birthdate",
"sex",
"is_sterile",
+ "active",
"notes",
- "asset_type",
- "archived",
"farmos_uuid",
"drupal_id",
- "thumbnail_url",
"image_url",
- "thumbnail",
"image",
]
@@ -183,44 +73,57 @@ class AnimalAssetView(AssetMasterView):
g = grid
super().configure_grid(g)
model = self.app.model
- enum = self.app.enum
+
+ # name
+ g.set_link("name")
# animal_type
g.set_joiner("animal_type", lambda q: q.join(model.AnimalType))
g.set_sorter("animal_type", model.AnimalType.name)
- g.set_filter("animal_type", model.AnimalType.name)
-
- # birthdate
- g.set_renderer("birthdate", "date")
-
- # sex
- g.set_enum("sex", enum.ANIMAL_SEX)
+ g.set_filter("animal_type", model.AnimalType.name, label="Animal Type Name")
def configure_form(self, form):
f = form
super().configure_form(f)
- enum = self.app.enum
- animal = f.model_instance
+ animal = form.model_instance
# animal_type
f.set_node("animal_type", AnimalTypeRef(self.request))
- # sex
- if self.viewing and animal.sex is None:
- pass # TODO: dict enum widget does not handle null values well
- else:
- f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX))
- f.set_required("sex", False)
+ # notes
+ f.set_widget("notes", "notes")
+
+ # image
+ if animal.image_url:
+ f.set_widget("image", ImageWidget("animal image"))
+ f.set_default("image", animal.image_url)
+
+ def get_farmos_url(self, animal):
+ return self.app.get_farmos_url(f"/asset/{animal.drupal_id}")
+
+ def get_xref_buttons(self, animal):
+ buttons = super().get_xref_buttons(animal)
+
+ if animal.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_animals.view", uuid=animal.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
def defaults(config, **kwargs):
base = globals()
- AnimalTypeView = kwargs.get("AnimalTypeView", base["AnimalTypeView"])
- AnimalTypeView.defaults(config)
-
- AnimalAssetView = kwargs.get("AnimalAssetView", base["AnimalAssetView"])
- AnimalAssetView.defaults(config)
+ AnimalView = kwargs.get("AnimalView", base["AnimalView"])
+ AnimalView.defaults(config)
def includeme(config):
diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py
deleted file mode 100644
index dffaae7..0000000
--- a/src/wuttafarm/web/views/assets.py
+++ /dev/null
@@ -1,292 +0,0 @@
-# -*- 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 Assets
-"""
-
-from collections import OrderedDict
-
-from wuttaweb.forms.schema import WuttaDictEnum
-from wuttaweb.db import Session
-
-from wuttafarm.web.views import WuttaFarmMasterView
-from wuttafarm.db.model import Asset
-from wuttafarm.web.forms.schema import AssetParentRefs
-from wuttafarm.web.forms.widgets import ImageWidget
-
-
-def get_asset_type_enum(config):
- app = config.get_app()
- model = app.model
- session = Session()
- asset_types = OrderedDict()
- query = session.query(model.AssetType).order_by(model.AssetType.name)
- for asset_type in query:
- asset_types[asset_type.drupal_id] = asset_type.name
- return asset_types
-
-
-class AssetView(WuttaFarmMasterView):
- """
- Master view for Assets
- """
-
- model_class = Asset
- route_prefix = "assets"
- url_prefix = "/assets"
-
- farmos_refurl_path = "/assets"
-
- viewable = False
- creatable = False
- editable = False
- deletable = False
- model_is_versioned = False
-
- grid_columns = [
- "thumbnail",
- "drupal_id",
- "asset_name",
- "asset_type",
- "parents",
- "archived",
- ]
-
- sort_defaults = "asset_name"
-
- filter_defaults = {
- "asset_name": {"active": True, "verb": "contains"},
- "archived": {"active": True, "verb": "is_false"},
- }
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
-
- # thumbnail
- g.set_renderer("thumbnail", self.render_grid_thumbnail)
- g.set_label("thumbnail", "", column_only=True)
- g.set_centered("thumbnail")
-
- # drupal_id
- g.set_label("drupal_id", "ID", column_only=True)
-
- # asset_name
- g.set_link("asset_name")
-
- # asset_type
- g.set_enum("asset_type", get_asset_type_enum(self.config))
-
- # parents
- g.set_renderer("parents", self.render_parents_for_grid)
-
- # view action links to final asset record
- def asset_url(asset, i):
- return self.request.route_url(
- f"{asset.asset_type}_assets.view", uuid=asset.uuid
- )
-
- g.add_action("view", icon="eye", url=asset_url)
-
- def render_parents_for_grid(self, asset, field, value):
- parents = [str(p.parent) for p in asset._parents]
- return ", ".join(parents)
-
- def grid_row_class(self, asset, data, i):
- """ """
- if asset.archived:
- return "has-background-warning"
- return None
-
-
-class AssetTypeMasterView(WuttaFarmMasterView):
- """
- Base class for "Asset Type" master views.
-
- A bit of a misnmer perhaps, this is *not* for the actual AssetType
- model, but rather the "secondary" types, e.g. AnimalType,
- LandType etc.
- """
-
-
-class AssetMasterView(WuttaFarmMasterView):
- """
- Base class for Asset master views
- """
-
- sort_defaults = "asset_name"
-
- filter_defaults = {
- "asset_name": {"active": True, "verb": "contains"},
- "archived": {"active": True, "verb": "is_false"},
- }
-
- def get_fallback_templates(self, template):
- templates = super().get_fallback_templates(template)
-
- if self.viewing:
- templates.insert(0, "/assets/master/view.mako")
-
- return templates
-
- def get_query(self, session=None):
- """ """
- model = self.app.model
- model_class = self.get_model_class()
- session = session or self.Session()
- return session.query(model_class).join(model.Asset)
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
- model = self.app.model
-
- # thumbnail
- g.set_renderer("thumbnail", self.render_grid_thumbnail)
- g.set_label("thumbnail", "", column_only=True)
- g.set_centered("thumbnail")
-
- # drupal_id
- g.set_label("drupal_id", "ID", column_only=True)
- g.set_sorter("drupal_id", model.Asset.drupal_id)
- g.set_filter("drupal_id", model.Asset.drupal_id)
-
- # asset_name
- g.set_link("asset_name")
- g.set_sorter("asset_name", model.Asset.asset_name)
- g.set_filter("asset_name", model.Asset.asset_name)
-
- # parents
- g.set_renderer("parents", self.render_parents_for_grid)
-
- # archived
- g.set_renderer("archived", "boolean")
- g.set_sorter("archived", model.Asset.archived)
- g.set_filter("archived", model.Asset.archived)
-
- def render_parents_for_grid(self, asset, field, value):
- parents = [str(p.parent) for p in asset.asset._parents]
- return ", ".join(parents)
-
- def grid_row_class(self, asset, data, i):
- """ """
- if asset.archived:
- return "has-background-warning"
- return None
-
- def configure_form(self, form):
- f = form
- super().configure_form(f)
- asset = form.model_instance
-
- # asset_type
- if self.creating:
- f.remove("asset_type")
- else:
- f.set_node(
- "asset_type",
- WuttaDictEnum(self.request, get_asset_type_enum(self.config)),
- )
- f.set_readonly("asset_type")
-
- # parents
- if self.creating or self.editing:
- f.remove("parents") # TODO: add support for this
- else:
- f.set_node("parents", AssetParentRefs(self.request))
- f.set_default("parents", [p.parent_uuid for p in asset.asset._parents])
-
- # notes
- f.set_widget("notes", "notes")
-
- # thumbnail_url
- if self.creating or self.editing:
- f.remove("thumbnail_url")
-
- # image_url
- if self.creating or self.editing:
- f.remove("image_url")
-
- # thumbnail
- if self.creating or self.editing:
- f.remove("thumbnail")
- elif asset.thumbnail_url:
- f.set_widget("thumbnail", ImageWidget("animal thumbnail"))
- f.set_default("thumbnail", asset.thumbnail_url)
-
- # image
- if self.creating or self.editing:
- f.remove("image")
- elif asset.image_url:
- f.set_widget("image", ImageWidget("animal image"))
- f.set_default("image", asset.image_url)
-
- def objectify(self, form):
- asset = super().objectify(form)
-
- if self.creating:
- model_class = self.get_model_class()
- asset.asset_type = model_class.__wutta_hint__["farmos_asset_type"]
-
- return asset
-
- def get_farmos_url(self, asset):
- return self.app.get_farmos_url(f"/asset/{asset.drupal_id}")
-
- def get_xref_buttons(self, asset):
- buttons = super().get_xref_buttons(asset)
-
- if asset.farmos_uuid:
-
- # TODO
- route = None
- if asset.asset_type == "animal":
- route = "farmos_animals.view"
- elif asset.asset_type == "group":
- route = "farmos_groups.view"
- elif asset.asset_type == "land":
- route = "farmos_land_assets.view"
- elif asset.asset_type == "structure":
- route = "farmos_structures.view"
-
- if route:
- buttons.append(
- self.make_button(
- "View farmOS record",
- primary=True,
- url=self.request.route_url(route, uuid=asset.farmos_uuid),
- icon_left="eye",
- )
- )
-
- return buttons
-
-
-def defaults(config, **kwargs):
- base = globals()
-
- AssetView = kwargs.get("AssetView", base["AssetView"])
- AssetView.defaults(config)
-
-
-def includeme(config):
- defaults(config)
diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py
index 121e631..cd68b78 100644
--- a/src/wuttafarm/web/views/common.py
+++ b/src/wuttafarm/web/views/common.py
@@ -54,17 +54,12 @@ class CommonView(base.CommonView):
"activity_logs.list",
"activity_logs.view",
"activity_logs.versions",
- "animal_types.create",
- "animal_types.edit",
"animal_types.list",
"animal_types.view",
"animal_types.versions",
- "animal_assets.create",
- "animal_assets.edit",
- "animal_assets.list",
- "animal_assets.view",
- "animal_assets.versions",
- "assets.list",
+ "animals.list",
+ "animals.view",
+ "animals.versions",
"asset_types.list",
"asset_types.view",
"asset_types.versions",
@@ -90,13 +85,9 @@ class CommonView(base.CommonView):
"farmos_structures.view",
"farmos_users.list",
"farmos_users.view",
- "group_asests.create",
- "group_asests.edit",
- "group_asests.list",
- "group_asests.view",
- "group_asests.versions",
- "land_assets.create",
- "land_assets.edit",
+ "groups.list",
+ "groups.view",
+ "groups.versions",
"land_assets.list",
"land_assets.view",
"land_assets.versions",
@@ -109,11 +100,9 @@ class CommonView(base.CommonView):
"structure_types.list",
"structure_types.view",
"structure_types.versions",
- "structure_assets.create",
- "structure_assets.edit",
- "structure_assets.list",
- "structure_assets.view",
- "structure_assets.versions",
+ "structures.list",
+ "structures.view",
+ "structures.versions",
]
for perm in site_admin_perms:
auth.grant_permission(site_admin, perm)
diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py
index c9c2887..d181a02 100644
--- a/src/wuttafarm/web/views/farmos/animals.py
+++ b/src/wuttafarm/web/views/farmos/animals.py
@@ -251,17 +251,15 @@ class AnimalView(FarmOSMasterView):
]
if wf_animal := (
- session.query(model.Asset)
- .filter(model.Asset.farmos_uuid == animal["uuid"])
+ session.query(model.Animal)
+ .filter(model.Animal.farmos_uuid == animal["uuid"])
.first()
):
buttons.append(
self.make_button(
f"View {self.app.get_title()} record",
primary=True,
- url=self.request.route_url(
- "animal_assets.view", uuid=wf_animal.uuid
- ),
+ url=self.request.route_url("animals.view", uuid=wf_animal.uuid),
icon_left="eye",
)
)
diff --git a/src/wuttafarm/web/views/farmos/groups.py b/src/wuttafarm/web/views/farmos/groups.py
index c6748c4..df54b04 100644
--- a/src/wuttafarm/web/views/farmos/groups.py
+++ b/src/wuttafarm/web/views/farmos/groups.py
@@ -166,15 +166,15 @@ class GroupView(FarmOSMasterView):
]
if wf_group := (
- session.query(model.GroupAsset)
- .filter(model.GroupAsset.farmos_uuid == group["uuid"])
+ session.query(model.Group)
+ .filter(model.Group.farmos_uuid == group["uuid"])
.first()
):
buttons.append(
self.make_button(
f"View {self.app.get_title()} record",
primary=True,
- url=self.request.route_url("group_assets.view", uuid=wf_group.uuid),
+ url=self.request.route_url("groups.view", uuid=wf_group.uuid),
icon_left="eye",
)
)
diff --git a/src/wuttafarm/web/views/farmos/structures.py b/src/wuttafarm/web/views/farmos/structures.py
index 550f432..618c2fa 100644
--- a/src/wuttafarm/web/views/farmos/structures.py
+++ b/src/wuttafarm/web/views/farmos/structures.py
@@ -211,8 +211,8 @@ class StructureView(FarmOSMasterView):
]
if wf_structure := (
- session.query(model.StructureAsset)
- .filter(model.StructureAsset.farmos_uuid == structure["uuid"])
+ session.query(model.Structure)
+ .filter(model.Structure.farmos_uuid == structure["uuid"])
.first()
):
buttons.append(
@@ -220,7 +220,7 @@ class StructureView(FarmOSMasterView):
f"View {self.app.get_title()} record",
primary=True,
url=self.request.route_url(
- "structure_assets.view", uuid=wf_structure.uuid
+ "structures.view", uuid=wf_structure.uuid
),
icon_left="eye",
)
diff --git a/src/wuttafarm/web/views/groups.py b/src/wuttafarm/web/views/groups.py
index 21d7ed4..5f2746b 100644
--- a/src/wuttafarm/web/views/groups.py
+++ b/src/wuttafarm/web/views/groups.py
@@ -23,37 +23,78 @@
Master view for Groups
"""
-from wuttafarm.web.views.assets import AssetMasterView
-from wuttafarm.db.model.groups import GroupAsset
+from wuttafarm.db.model.groups import Group
+from wuttafarm.web.views import WuttaFarmMasterView
-class GroupView(AssetMasterView):
+class GroupView(WuttaFarmMasterView):
"""
Master view for Groups
"""
- model_class = GroupAsset
- route_prefix = "group_assets"
- url_prefix = "/assets/group"
+ model_class = Group
+ route_prefix = "groups"
+ url_prefix = "/groups"
farmos_refurl_path = "/assets/group"
grid_columns = [
- "thumbnail",
- "drupal_id",
- "asset_name",
- "archived",
+ "name",
+ "is_location",
+ "is_fixed",
+ "active",
]
+ sort_defaults = "name"
+
+ filter_defaults = {
+ "name": {"active": True, "verb": "contains"},
+ }
+
form_fields = [
- "asset_name",
+ "name",
+ "is_location",
+ "is_fixed",
+ "active",
"notes",
- "asset_type",
- "archived",
"farmos_uuid",
"drupal_id",
]
+ def configure_grid(self, grid):
+ g = grid
+ super().configure_grid(g)
+
+ # name
+ g.set_link("name")
+
+ def configure_form(self, form):
+ f = form
+ super().configure_form(f)
+
+ # notes
+ f.set_widget("notes", "notes")
+
+ def get_farmos_url(self, group):
+ return self.app.get_farmos_url(f"/asset/{group.drupal_id}")
+
+ def get_xref_buttons(self, group):
+ buttons = super().get_xref_buttons(group)
+
+ if group.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_groups.view", uuid=group.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
def defaults(config, **kwargs):
base = globals()
diff --git a/src/wuttafarm/web/views/land_assets.py b/src/wuttafarm/web/views/land_assets.py
new file mode 100644
index 0000000..18f7a3d
--- /dev/null
+++ b/src/wuttafarm/web/views/land_assets.py
@@ -0,0 +1,117 @@
+# -*- 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 Land Assets
+"""
+
+from wuttafarm.db.model.land import LandAsset
+from wuttafarm.web.views import WuttaFarmMasterView
+from wuttafarm.web.forms.schema import LandTypeRef
+
+
+class LandAssetView(WuttaFarmMasterView):
+ """
+ Master view for Land Assets
+ """
+
+ model_class = LandAsset
+ route_prefix = "land_assets"
+ url_prefix = "/land-assets"
+
+ farmos_refurl_path = "/assets/land"
+
+ grid_columns = [
+ "name",
+ "land_type",
+ "is_location",
+ "is_fixed",
+ "notes",
+ "active",
+ ]
+
+ sort_defaults = "name"
+
+ filter_defaults = {
+ "name": {"active": True, "verb": "contains"},
+ }
+
+ form_fields = [
+ "name",
+ "land_type",
+ "is_location",
+ "is_fixed",
+ "notes",
+ "active",
+ "farmos_uuid",
+ "drupal_id",
+ ]
+
+ def configure_grid(self, grid):
+ g = grid
+ super().configure_grid(g)
+ model = self.app.model
+
+ # name
+ g.set_link("name")
+
+ # land_type
+ g.set_joiner("land_type", lambda q: q.join(model.LandType))
+ g.set_sorter("land_type", model.LandType.name)
+ g.set_filter("land_type", model.LandType.name, label="Land Type Name")
+
+ def configure_form(self, form):
+ f = form
+ super().configure_form(f)
+
+ # land_type
+ f.set_node("land_type", LandTypeRef(self.request))
+
+ def get_farmos_url(self, land):
+ return self.app.get_farmos_url(f"/asset/{land.drupal_id}")
+
+ def get_xref_buttons(self, land_asset):
+ buttons = super().get_xref_buttons(land_asset)
+
+ if land_asset.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_land_assets.view", uuid=land_asset.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"])
+ LandAssetView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)
diff --git a/src/wuttafarm/web/views/land.py b/src/wuttafarm/web/views/land_types.py
similarity index 53%
rename from src/wuttafarm/web/views/land.py
rename to src/wuttafarm/web/views/land_types.py
index aad15e7..21bfabc 100644
--- a/src/wuttafarm/web/views/land.py
+++ b/src/wuttafarm/web/views/land_types.py
@@ -23,14 +23,11 @@
Master view for Land Types
"""
-from webhelpers2.html import HTML, tags
-
from wuttafarm.db.model.land import LandType, LandAsset
-from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
-from wuttafarm.web.forms.schema import LandTypeRef
+from wuttafarm.web.views import WuttaFarmMasterView
-class LandTypeView(AssetTypeMasterView):
+class LandTypeView(WuttaFarmMasterView):
"""
Master view for Land Types
"""
@@ -60,13 +57,13 @@ class LandTypeView(AssetTypeMasterView):
rows_viewable = True
row_grid_columns = [
- "asset_name",
+ "name",
"is_location",
"is_fixed",
- "archived",
+ "active",
]
- rows_sort_defaults = "asset_name"
+ rows_sort_defaults = "name"
def configure_grid(self, grid):
g = grid
@@ -95,102 +92,27 @@ class LandTypeView(AssetTypeMasterView):
def get_row_grid_data(self, land_type):
model = self.app.model
session = self.Session()
- return (
- session.query(model.LandAsset)
- .join(model.Asset)
- .filter(model.LandAsset.land_type == land_type)
+ return session.query(model.LandAsset).filter(
+ model.LandAsset.land_type == land_type
)
def configure_row_grid(self, grid):
g = grid
super().configure_row_grid(g)
- model = self.app.model
- # asset_name
- g.set_link("asset_name")
- g.set_sorter("asset_name", model.Asset.asset_name)
- g.set_filter("asset_name", model.Asset.asset_name)
-
- # is_location
- g.set_renderer("is_location", "boolean")
- g.set_sorter("is_location", model.Asset.is_location)
- g.set_filter("is_location", model.Asset.is_location)
-
- # is_fixed
- g.set_renderer("is_fixed", "boolean")
- g.set_sorter("is_fixed", model.Asset.is_fixed)
- g.set_filter("is_fixed", model.Asset.is_fixed)
-
- # archived
- g.set_renderer("archived", "boolean")
- g.set_sorter("archived", model.Asset.archived)
- g.set_filter("archived", model.Asset.archived)
+ # name
+ g.set_link("name")
def get_row_action_url_view(self, land_asset, i):
return self.request.route_url("land_assets.view", uuid=land_asset.uuid)
-class LandAssetView(AssetMasterView):
- """
- Master view for Land Assets
- """
-
- model_class = LandAsset
- route_prefix = "land_assets"
- url_prefix = "/assets/land"
-
- farmos_refurl_path = "/assets/land"
-
- grid_columns = [
- "thumbnail",
- "drupal_id",
- "asset_name",
- "land_type",
- "parents",
- "archived",
- ]
-
- form_fields = [
- "asset_name",
- "parents",
- "notes",
- "asset_type",
- "land_type",
- "is_location",
- "is_fixed",
- "archived",
- "farmos_uuid",
- "drupal_id",
- ]
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
- model = self.app.model
-
- # land_type
- g.set_joiner("land_type", lambda q: q.join(model.LandType))
- g.set_sorter("land_type", model.LandType.name)
- g.set_filter("land_type", model.LandType.name, label="Land Type Name")
-
- def configure_form(self, form):
- f = form
- super().configure_form(f)
- land = f.model_instance
-
- # land_type
- f.set_node("land_type", LandTypeRef(self.request))
-
-
def defaults(config, **kwargs):
base = globals()
LandTypeView = kwargs.get("LandTypeView", base["LandTypeView"])
LandTypeView.defaults(config)
- LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"])
- LandAssetView.defaults(config)
-
def includeme(config):
defaults(config)
diff --git a/src/wuttafarm/web/views/master.py b/src/wuttafarm/web/views/master.py
index 0e25a30..7ff165b 100644
--- a/src/wuttafarm/web/views/master.py
+++ b/src/wuttafarm/web/views/master.py
@@ -23,8 +23,6 @@
Base class for WuttaFarm master views
"""
-from webhelpers2.html import tags
-
from wuttaweb.views import MasterView
@@ -39,14 +37,12 @@ class WuttaFarmMasterView(MasterView):
"farmos_uuid": "farmOS UUID",
"drupal_id": "Drupal ID",
"image_url": "Image URL",
- "thumbnail_url": "Thumbnail URL",
}
row_labels = {
"farmos_uuid": "farmOS UUID",
"drupal_id": "Drupal ID",
"image_url": "Image URL",
- "thumbnail_url": "Thumbnail URL",
}
def get_farmos_url(self, obj):
@@ -59,13 +55,6 @@ class WuttaFarmMasterView(MasterView):
return context
- def render_grid_thumbnail(self, obj, field, value):
- if obj.thumbnail_url:
- return tags.image(
- obj.thumbnail_url, f"thumbnail for {self.get_model_title()}"
- )
- return None
-
def get_xref_buttons(self, obj):
url = self.get_farmos_url(obj)
if url:
@@ -79,24 +68,3 @@ class WuttaFarmMasterView(MasterView):
)
]
return []
-
- def configure_form(self, form):
- """ """
- f = form
- super().configure_form(f)
-
- # farmos_uuid
- if self.creating:
- f.remove("farmos_uuid")
- else:
- f.set_readonly("farmos_uuid")
-
- # drupal_id
- if self.creating:
- f.remove("drupal_id")
- else:
- f.set_readonly("drupal_id")
-
- def persist(self, obj, session=None):
- super().persist(obj, session)
- self.app.export_to_farmos(obj, require=False)
diff --git a/src/wuttafarm/web/views/structure_types.py b/src/wuttafarm/web/views/structure_types.py
new file mode 100644
index 0000000..ca85fb9
--- /dev/null
+++ b/src/wuttafarm/web/views/structure_types.py
@@ -0,0 +1,118 @@
+# -*- 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 Structure Types
+"""
+
+from wuttafarm.db.model.structures import StructureType, Structure
+from wuttafarm.web.views import WuttaFarmMasterView
+
+
+class StructureTypeView(WuttaFarmMasterView):
+ """
+ Master view for Structure Types
+ """
+
+ model_class = StructureType
+ route_prefix = "structure_types"
+ url_prefix = "/structure-types"
+
+ grid_columns = [
+ "name",
+ ]
+
+ sort_defaults = "name"
+
+ filter_defaults = {
+ "name": {"active": True, "verb": "contains"},
+ }
+
+ form_fields = [
+ "name",
+ "farmos_uuid",
+ "drupal_id",
+ ]
+
+ has_rows = True
+ row_model_class = Structure
+ rows_viewable = True
+
+ row_grid_columns = [
+ "name",
+ "is_location",
+ "is_fixed",
+ "active",
+ ]
+
+ rows_sort_defaults = "name"
+
+ def configure_grid(self, grid):
+ g = grid
+ super().configure_grid(g)
+
+ # name
+ g.set_link("name")
+
+ def get_xref_buttons(self, structure_type):
+ buttons = super().get_xref_buttons(structure_type)
+
+ if structure_type.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_structure_types.view", uuid=structure_type.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
+ def get_row_grid_data(self, structure_type):
+ model = self.app.model
+ session = self.Session()
+ return session.query(model.Structure).filter(
+ model.Structure.structure_type == structure_type
+ )
+
+ def configure_row_grid(self, grid):
+ g = grid
+ super().configure_row_grid(g)
+
+ # name
+ g.set_link("name")
+
+ def get_row_action_url_view(self, structure, i):
+ return self.request.route_url("structures.view", uuid=structure.uuid)
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ StructureTypeView = kwargs.get("StructureTypeView", base["StructureTypeView"])
+ StructureTypeView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)
diff --git a/src/wuttafarm/web/views/structures.py b/src/wuttafarm/web/views/structures.py
index aa9bf31..df58fda 100644
--- a/src/wuttafarm/web/views/structures.py
+++ b/src/wuttafarm/web/views/structures.py
@@ -23,23 +23,29 @@
Master view for Structures
"""
-from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
-from wuttafarm.db.model import StructureType, StructureAsset
+from wuttafarm.db.model.structures import Structure
+from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.web.forms.schema import StructureTypeRef
from wuttafarm.web.forms.widgets import ImageWidget
-class StructureTypeView(AssetTypeMasterView):
+class StructureView(WuttaFarmMasterView):
"""
- Master view for Structure Types
+ Master view for Structures
"""
- model_class = StructureType
- route_prefix = "structure_types"
- url_prefix = "/structure-types"
+ model_class = Structure
+ route_prefix = "structures"
+ url_prefix = "/structures"
+
+ farmos_refurl_path = "/assets/structure"
grid_columns = [
"name",
+ "structure_type",
+ "is_location",
+ "is_fixed",
+ "active",
]
sort_defaults = "name"
@@ -50,119 +56,14 @@ class StructureTypeView(AssetTypeMasterView):
form_fields = [
"name",
- "farmos_uuid",
- "drupal_id",
- ]
-
- has_rows = True
- row_model_class = StructureAsset
- rows_viewable = True
-
- row_grid_columns = [
- "asset_name",
+ "structure_type",
"is_location",
"is_fixed",
- "archived",
- ]
-
- rows_sort_defaults = "asset_name"
-
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
-
- # name
- g.set_link("name")
-
- def get_xref_buttons(self, structure_type):
- buttons = super().get_xref_buttons(structure_type)
-
- if structure_type.farmos_uuid:
- buttons.append(
- self.make_button(
- "View farmOS record",
- primary=True,
- url=self.request.route_url(
- "farmos_structure_types.view", uuid=structure_type.farmos_uuid
- ),
- icon_left="eye",
- )
- )
-
- return buttons
-
- def get_row_grid_data(self, structure_type):
- model = self.app.model
- session = self.Session()
- return (
- session.query(model.StructureAsset)
- .join(model.Asset)
- .filter(model.StructureAsset.structure_type == structure_type)
- )
-
- def configure_row_grid(self, grid):
- g = grid
- super().configure_row_grid(g)
- model = self.app.model
-
- # asset_name
- g.set_link("asset_name")
- g.set_sorter("asset_name", model.Asset.asset_name)
- g.set_filter("asset_name", model.Asset.asset_name)
-
- # is_location
- g.set_renderer("is_location", "boolean")
- g.set_sorter("is_location", model.Asset.is_location)
- g.set_filter("is_location", model.Asset.is_location)
-
- # is_fixed
- g.set_renderer("is_fixed", "boolean")
- g.set_sorter("is_fixed", model.Asset.is_fixed)
- g.set_filter("is_fixed", model.Asset.is_fixed)
-
- # archived
- g.set_renderer("archived", "boolean")
- g.set_sorter("archived", model.Asset.archived)
- g.set_filter("archived", model.Asset.archived)
-
- def get_row_action_url_view(self, structure, i):
- return self.request.route_url("structure_assets.view", uuid=structure.uuid)
-
-
-class StructureAssetView(AssetMasterView):
- """
- Master view for Structures
- """
-
- model_class = StructureAsset
- route_prefix = "structure_assets"
- url_prefix = "/asset/structures"
-
- farmos_refurl_path = "/assets/structure"
-
- grid_columns = [
- "thumbnail",
- "drupal_id",
- "asset_name",
- "structure_type",
- "parents",
- "archived",
- ]
-
- form_fields = [
- "asset_name",
- "parents",
"notes",
- "asset_type",
- "structure_type",
- "is_location",
- "is_fixed",
- "archived",
+ "active",
"farmos_uuid",
"drupal_id",
- "thumbnail_url",
"image_url",
- "thumbnail",
"image",
]
@@ -171,6 +72,9 @@ class StructureAssetView(AssetMasterView):
super().configure_grid(g)
model = self.app.model
+ # name
+ g.set_link("name")
+
# structure_type
g.set_joiner("structure_type", lambda q: q.join(model.StructureType))
g.set_sorter("structure_type", model.StructureType.name)
@@ -186,15 +90,37 @@ class StructureAssetView(AssetMasterView):
# structure_type
f.set_node("structure_type", StructureTypeRef(self.request))
+ # image
+ if structure.image_url:
+ f.set_widget("image", ImageWidget("structure image"))
+ f.set_default("image", structure.image_url)
+
+ def get_farmos_url(self, structure):
+ return self.app.get_farmos_url(f"/asset/{structure.drupal_id}")
+
+ def get_xref_buttons(self, structure):
+ buttons = super().get_xref_buttons(structure)
+
+ if structure.farmos_uuid:
+ buttons.append(
+ self.make_button(
+ "View farmOS record",
+ primary=True,
+ url=self.request.route_url(
+ "farmos_structures.view", uuid=structure.farmos_uuid
+ ),
+ icon_left="eye",
+ )
+ )
+
+ return buttons
+
def defaults(config, **kwargs):
base = globals()
- StructureTypeView = kwargs.get("StructureTypeView", base["StructureTypeView"])
- StructureTypeView.defaults(config)
-
- StructureAssetView = kwargs.get("StructureAssetView", base["StructureAssetView"])
- StructureAssetView.defaults(config)
+ StructureView = kwargs.get("StructureView", base["StructureView"])
+ StructureView.defaults(config)
def includeme(config):