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 new file mode 100644 index 0000000..cd0a34a --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/d6e8d16d6854_add_generic_animal_assets.py @@ -0,0 +1,333 @@ +"""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/model/__init__.py b/src/wuttafarm/db/model/__init__.py index e8bce3f..a549879 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 +from .assets import AssetType, Asset from .land import LandType, LandAsset, LandAssetParent from .structures import StructureType, Structure -from .animals import AnimalType, Animal +from .animals import AnimalType, AnimalAsset 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 1aec805..548be86 100644 --- a/src/wuttafarm/db/model/animals.py +++ b/src/wuttafarm/db/model/animals.py @@ -28,6 +28,8 @@ from sqlalchemy import orm from wuttjamaican.db import model +from wuttafarm.db.model.assets import AssetMixin, add_asset_proxies + class AnimalType(model.Base): """ @@ -94,28 +96,19 @@ class AnimalType(model.Base): return self.name or "" -class Animal(model.Base): +class AnimalAsset(AssetMixin, model.Base): """ - Represents an animal from farmOS + Represents an animal asset from farmOS """ - __tablename__ = "animal" + __tablename__ = "asset_animal" __versioned__ = {} __wutta_hint__ = { - "model_title": "Animal", - "model_title_plural": "Animals", + "model_title": "Animal Asset", + "model_title_plural": "Animal Assets", + "farmos_asset_type": "animal", } - 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", @@ -148,56 +141,5 @@ class Animal(model.Base): """, ) - archived = sa.Column( - sa.Boolean(), - nullable=False, - default=False, - doc=""" - Whether the animal is archived. - """, - ) - 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. - """, - ) - - thumbnail_url = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Optional thumbnail 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 "" +add_asset_proxies(AnimalAsset) diff --git a/src/wuttafarm/db/model/assets.py b/src/wuttafarm/db/model/assets.py index 581be62..85c7eb4 100644 --- a/src/wuttafarm/db/model/assets.py +++ b/src/wuttafarm/db/model/assets.py @@ -25,6 +25,7 @@ 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 @@ -80,3 +81,127 @@ 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. + """, + ) + + 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") diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index adc63d0..6d8c573 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -102,7 +102,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): importers["StructureType"] = StructureTypeImporter importers["Structure"] = StructureImporter importers["AnimalType"] = AnimalTypeImporter - importers["Animal"] = AnimalImporter + importers["AnimalAsset"] = AnimalAssetImporter importers["Group"] = GroupImporter importers["LogType"] = LogTypeImporter importers["ActivityLog"] = ActivityLogImporter @@ -176,17 +176,77 @@ class ActivityLogImporter(FromFarmOS, ToWutta): } -class AnimalImporter(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", + "notes", + "archived", + "image_url", + "thumbnail_url", + ] + ) + return fields + + 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" + + return { + "farmos_uuid": UUID(asset["id"]), + "drupal_id": asset["attributes"]["drupal_internal__id"], + "asset_name": asset["attributes"]["name"], + "archived": archived, + "notes": notes, + "image_url": image_url, + "thumbnail_url": thumbnail_url, + } + + +class AnimalAssetImporter(AssetImporterBase): """ farmOS API → WuttaFarm importer for Animals """ - model_class = model.Animal + model_class = model.AnimalAsset supported_fields = [ "farmos_uuid", "drupal_id", - "name", + "asset_type", + "asset_name", "animal_type_uuid", "sex", "is_sterile", @@ -214,27 +274,18 @@ class AnimalImporter(FromFarmOS, ToWutta): def normalize_source_object(self, animal): """ """ animal_type_uuid = None - image_url = None - thumbnail_url = None if relationships := animal.get("relationships"): if animal_type := relationships.get("animal_type"): if animal_type["data"]: - if animal_type := self.animal_types_by_farmos_uuid.get( + if wf_animal_type := self.animal_types_by_farmos_uuid.get( UUID(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"] - thumbnail_url = image_style["thumbnail"] + animal_type_uuid = wf_animal_type.uuid + else: + log.warning( + "animal type not found: %s", animal_type["data"]["id"] + ) if not animal_type_uuid: log.warning("missing/invalid animal_type for farmOS Animal: %s", animal) @@ -251,27 +302,17 @@ class AnimalImporter(FromFarmOS, ToWutta): else: sterile = animal["attributes"]["is_castrated"] - if notes := animal["attributes"]["notes"]: - notes = notes["value"] - - if self.farmos_4x: - archived = animal["attributes"]["archived"] - else: - archived = animal["attributes"]["status"] == "archived" - - 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, - "archived": archived, - "notes": notes, - "image_url": image_url, - "thumbnail_url": thumbnail_url, - } + 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 class AnimalTypeImporter(FromFarmOS, ToWutta): diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 3e5bb46..071e5b6 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -45,10 +45,16 @@ class WuttaFarmMenuHandler(base.MenuHandler): "type": "menu", "items": [ { - "title": "Animals", - "route": "animals", - "perm": "animals.list", + "title": "All Assets", + "route": "assets", + "perm": "assets.list", }, + { + "title": "Animal", + "route": "animal_assets", + "perm": "animal_assets.list", + }, + {"type": "sep"}, { "title": "Groups", "route": "groups", diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index a4d12dd..d27e6d9 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -42,9 +42,9 @@ 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_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") diff --git a/src/wuttafarm/web/views/animal_types.py b/src/wuttafarm/web/views/animal_types.py deleted file mode 100644 index acb1bd9..0000000 --- a/src/wuttafarm/web/views/animal_types.py +++ /dev/null @@ -1,128 +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 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", - "archived", - ] - - 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 5758d68..b415cd6 100644 --- a/src/wuttafarm/web/views/animals.py +++ b/src/wuttafarm/web/views/animals.py @@ -25,25 +25,129 @@ Master view for Animals from wuttaweb.forms.schema import WuttaDictEnum -from wuttafarm.db.model.animals import Animal +from wuttafarm.db.model import AnimalType, AnimalAsset from wuttafarm.web.views import WuttaFarmMasterView +from wuttafarm.web.views.assets import AssetMasterView from wuttafarm.web.forms.schema import AnimalTypeRef from wuttafarm.web.forms.widgets import ImageWidget -class AnimalView(WuttaFarmMasterView): +class AnimalTypeView(WuttaFarmMasterView): """ - Master view for Animals + Master view for Animal Types """ - model_class = Animal - route_prefix = "animals" - url_prefix = "/animals" + 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 = 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 = { - "name": "Asset Name", "animal_type": "Species/Breed", "is_sterile": "Sterile", } @@ -51,7 +155,7 @@ class AnimalView(WuttaFarmMasterView): grid_columns = [ "thumbnail", "drupal_id", - "name", + "asset_name", "animal_type", "birthdate", "is_sterile", @@ -59,15 +163,8 @@ class AnimalView(WuttaFarmMasterView): "archived", ] - sort_defaults = "name" - - filter_defaults = { - "name": {"active": True, "verb": "contains"}, - "archived": {"active": True, "verb": "is_false"}, - } - form_fields = [ - "name", + "asset_name", "animal_type", "birthdate", "sex", @@ -89,21 +186,10 @@ class AnimalView(WuttaFarmMasterView): model = self.app.model enum = self.app.enum - # 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) - - # 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, label="Animal Type Name") + g.set_filter("animal_type", model.AnimalType.name) # birthdate g.set_renderer("birthdate", "date") @@ -111,17 +197,10 @@ class AnimalView(WuttaFarmMasterView): # sex g.set_enum("sex", enum.ANIMAL_SEX) - def grid_row_class(self, animal, data, i): - """ """ - if animal.archived: - return "has-background-warning" - return None - def configure_form(self, form): f = form super().configure_form(f) enum = self.app.enum - animal = form.model_instance # animal_type f.set_node("animal_type", AnimalTypeRef(self.request)) @@ -129,64 +208,15 @@ class AnimalView(WuttaFarmMasterView): # sex f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX)) - # notes - f.set_widget("notes", "notes") - - # asset_type - if self.creating: - f.remove("asset_type") - else: - f.set_default("asset_type", "Animal") - f.set_readonly("asset_type") - - # 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 animal.thumbnail_url: - f.set_widget("thumbnail", ImageWidget("animal thumbnail")) - f.set_default("thumbnail", animal.thumbnail_url) - - # image - if self.creating or self.editing: - f.remove("image") - elif 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() - AnimalView = kwargs.get("AnimalView", base["AnimalView"]) - AnimalView.defaults(config) + AnimalTypeView = kwargs.get("AnimalTypeView", base["AnimalTypeView"]) + AnimalTypeView.defaults(config) + + AnimalAssetView = kwargs.get("AnimalAssetView", base["AnimalAssetView"]) + AnimalAssetView.defaults(config) def includeme(config): diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py new file mode 100644 index 0000000..b8b1dfc --- /dev/null +++ b/src/wuttafarm/web/views/assets.py @@ -0,0 +1,236 @@ +# -*- 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 wuttafarm.web.views import WuttaFarmMasterView +from wuttafarm.db.model import Asset +from wuttafarm.web.forms.widgets import ImageWidget + + +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 + + grid_columns = [ + "thumbnail", + "drupal_id", + "asset_name", + "asset_type", + "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", self.get_asset_type_enum()) + + # 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 get_asset_type_enum(self): + model = self.app.model + session = self.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 + + def grid_row_class(self, asset, data, i): + """ """ + if asset.archived: + return "has-background-warning" + return None + + +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_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) + + # archived + g.set_renderer("archived", "boolean") + g.set_sorter("archived", model.Asset.archived) + g.set_filter("archived", model.Asset.archived) + + 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_readonly("asset_type") + + # 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" + + 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/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py index d181a02..c9c2887 100644 --- a/src/wuttafarm/web/views/farmos/animals.py +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -251,15 +251,17 @@ class AnimalView(FarmOSMasterView): ] if wf_animal := ( - session.query(model.Animal) - .filter(model.Animal.farmos_uuid == animal["uuid"]) + session.query(model.Asset) + .filter(model.Asset.farmos_uuid == animal["uuid"]) .first() ): buttons.append( self.make_button( f"View {self.app.get_title()} record", primary=True, - url=self.request.route_url("animals.view", uuid=wf_animal.uuid), + url=self.request.route_url( + "animal_assets.view", uuid=wf_animal.uuid + ), icon_left="eye", ) )