From 2fc9c88cd50359755cd448b84e7e81057d4cc732 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Feb 2026 14:07:03 -0600 Subject: [PATCH] feat: convert group assets to use common base/mixin --- ...175624_use_shared_base_for_group_assets.py | 194 ++++++++++++++++++ src/wuttafarm/db/model/__init__.py | 2 +- src/wuttafarm/db/model/groups.py | 80 +------- src/wuttafarm/importing/farmos.py | 38 ++-- src/wuttafarm/web/menus.py | 11 +- src/wuttafarm/web/views/assets.py | 2 + src/wuttafarm/web/views/farmos/groups.py | 6 +- src/wuttafarm/web/views/groups.py | 78 +------ 8 files changed, 239 insertions(+), 172 deletions(-) create mode 100644 src/wuttafarm/db/alembic/versions/aecfd9175624_use_shared_base_for_group_assets.py 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 new file mode 100644 index 0000000..1295d40 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/aecfd9175624_use_shared_base_for_group_assets.py @@ -0,0 +1,194 @@ +"""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/model/__init__.py b/src/wuttafarm/db/model/__init__.py index 5b1625a..367bc1c 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -34,5 +34,5 @@ from .assets import AssetType, Asset, AssetParent from .land import LandType, LandAsset from .structures import StructureType, StructureAsset from .animals import AnimalType, AnimalAsset -from .groups import Group +from .groups import GroupAsset from .logs import LogType, ActivityLog diff --git a/src/wuttafarm/db/model/groups.py b/src/wuttafarm/db/model/groups.py index 2cec3eb..84453a7 100644 --- a/src/wuttafarm/db/model/groups.py +++ b/src/wuttafarm/db/model/groups.py @@ -23,85 +23,23 @@ 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 Group(model.Base): + +class GroupAsset(AssetMixin, model.Base): """ - Represents a "group" from farmOS + Represents a group asset from farmOS """ - __tablename__ = "group" + __tablename__ = "asset_group" __versioned__ = {} __wutta_hint__ = { - "model_title": "Group", - "model_title_plural": "Groups", + "model_title": "Group Asset", + "model_title_plural": "Group Assets", + "farmos_asset_type": "group", } - uuid = model.uuid_column() - 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. - """, - ) - - archived = sa.Column( - sa.Boolean(), - nullable=False, - default=False, - doc=""" - Whether the group is archived. - """, - ) - - 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 "" +add_asset_proxies(GroupAsset) diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 4410023..4f9db20 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -103,7 +103,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): importers["StructureAsset"] = StructureAssetImporter importers["AnimalType"] = AnimalTypeImporter importers["AnimalAsset"] = AnimalAssetImporter - importers["Group"] = GroupImporter + importers["GroupAsset"] = GroupAssetImporter importers["LogType"] = LogTypeImporter importers["ActivityLog"] = ActivityLogImporter return importers @@ -460,21 +460,25 @@ class AssetTypeImporter(FromFarmOS, ToWutta): } -class GroupImporter(FromFarmOS, ToWutta): +class GroupAssetImporter(AssetImporterBase): """ - farmOS API → WuttaFarm importer for Groups + farmOS API → WuttaFarm importer for Group Assets """ - model_class = model.Group + model_class = model.GroupAsset supported_fields = [ "farmos_uuid", "drupal_id", - "name", + "asset_type", + "asset_name", "is_location", "is_fixed", "notes", "archived", + "image_url", + "thumbnail_url", + "parents", ] def get_source_objects(self): @@ -484,23 +488,13 @@ class GroupImporter(FromFarmOS, ToWutta): def normalize_source_object(self, group): """ """ - if notes := group["attributes"]["notes"]: - notes = notes["value"] - - if self.farmos_4x: - archived = group["attributes"]["archived"] - else: - archived = group["attributes"]["status"] == "archived" - - 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"], - "archived": archived, - "notes": notes, - } + data = self.normalize_asset(group) + data.update( + { + "asset_type": "group", + } + ) + return data class LandAssetImporter(AssetImporterBase): diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index a7aa4b2..bdd2fbf 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -54,6 +54,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "animal_assets", "perm": "animal_assets.list", }, + { + "title": "Group", + "route": "group_assets", + "perm": "group_assets.list", + }, { "title": "Land", "route": "land_assets", @@ -65,12 +70,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "perm": "structure_assets.list", }, {"type": "sep"}, - { - "title": "Groups", - "route": "groups", - "perm": "groups.list", - }, - {"type": "sep"}, { "title": "Animal Types", "route": "animal_types", diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py index c4bb792..4798a64 100644 --- a/src/wuttafarm/web/views/assets.py +++ b/src/wuttafarm/web/views/assets.py @@ -239,6 +239,8 @@ class AssetMasterView(WuttaFarmMasterView): 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": diff --git a/src/wuttafarm/web/views/farmos/groups.py b/src/wuttafarm/web/views/farmos/groups.py index df54b04..c6748c4 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.Group) - .filter(model.Group.farmos_uuid == group["uuid"]) + session.query(model.GroupAsset) + .filter(model.GroupAsset.farmos_uuid == group["uuid"]) .first() ): buttons.append( self.make_button( f"View {self.app.get_title()} record", primary=True, - url=self.request.route_url("groups.view", uuid=wf_group.uuid), + url=self.request.route_url("group_assets.view", uuid=wf_group.uuid), icon_left="eye", ) ) diff --git a/src/wuttafarm/web/views/groups.py b/src/wuttafarm/web/views/groups.py index e3ae0ad..21d7ed4 100644 --- a/src/wuttafarm/web/views/groups.py +++ b/src/wuttafarm/web/views/groups.py @@ -23,40 +23,30 @@ Master view for Groups """ -from wuttafarm.db.model.groups import Group -from wuttafarm.web.views import WuttaFarmMasterView +from wuttafarm.web.views.assets import AssetMasterView +from wuttafarm.db.model.groups import GroupAsset -class GroupView(WuttaFarmMasterView): +class GroupView(AssetMasterView): """ Master view for Groups """ - model_class = Group - route_prefix = "groups" - url_prefix = "/groups" + model_class = GroupAsset + route_prefix = "group_assets" + url_prefix = "/assets/group" farmos_refurl_path = "/assets/group" - labels = { - "name": "Asset Name", - } - grid_columns = [ + "thumbnail", "drupal_id", - "name", + "asset_name", "archived", ] - sort_defaults = "name" - - filter_defaults = { - "name": {"active": True, "verb": "contains"}, - "archived": {"active": True, "verb": "is_false"}, - } - form_fields = [ - "name", + "asset_name", "notes", "asset_type", "archived", @@ -64,56 +54,6 @@ class GroupView(WuttaFarmMasterView): "drupal_id", ] - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # drupal_id - g.set_label("drupal_id", "ID", column_only=True) - - # name - g.set_link("name") - - def grid_row_class(self, group, data, i): - """ """ - if group.archived: - return "has-background-warning" - return None - - def configure_form(self, form): - f = form - super().configure_form(f) - - # notes - f.set_widget("notes", "notes") - - # asset_type - if self.creating: - f.remove("asset_type") - else: - f.set_default("asset_type", "Group") - f.set_readonly("asset_type") - - 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()