feat: add schema, import support for Asset.owners

This commit is contained in:
Lance Edgar 2026-03-02 19:44:52 -06:00
parent ce103137a5
commit eb16990b0b
7 changed files with 273 additions and 72 deletions

View file

@ -0,0 +1,114 @@
"""add Asset.owners
Revision ID: 12de43facb95
Revises: 85d4851e8292
Create Date: 2026-03-02 19:03:35.511398
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "12de43facb95"
down_revision: Union[str, None] = "85d4851e8292"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# asset_owner
op.create_table(
"asset_owner",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("user_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["asset_uuid"], ["asset.uuid"], name=op.f("fk_asset_owner_asset_uuid_asset")
),
sa.ForeignKeyConstraint(
["user_uuid"], ["user.uuid"], name=op.f("fk_asset_owner_user_uuid_user")
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_owner")),
)
op.create_table(
"asset_owner_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(
"user_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_owner_version")
),
)
op.create_index(
op.f("ix_asset_owner_version_end_transaction_id"),
"asset_owner_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_asset_owner_version_operation_type"),
"asset_owner_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_asset_owner_version_pk_transaction_id",
"asset_owner_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_asset_owner_version_pk_validity",
"asset_owner_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_asset_owner_version_transaction_id"),
"asset_owner_version",
["transaction_id"],
unique=False,
)
def downgrade() -> None:
# asset_owner
op.drop_index(
op.f("ix_asset_owner_version_transaction_id"), table_name="asset_owner_version"
)
op.drop_index(
"ix_asset_owner_version_pk_validity", table_name="asset_owner_version"
)
op.drop_index(
"ix_asset_owner_version_pk_transaction_id", table_name="asset_owner_version"
)
op.drop_index(
op.f("ix_asset_owner_version_operation_type"), table_name="asset_owner_version"
)
op.drop_index(
op.f("ix_asset_owner_version_end_transaction_id"),
table_name="asset_owner_version",
)
op.drop_table("asset_owner_version")
op.drop_table("asset_owner")

View file

@ -193,6 +193,19 @@ class Asset(model.Base):
creator=lambda parent: AssetParent(parent=parent),
)
_owners = orm.relationship(
"AssetOwner",
cascade="all, delete-orphan",
cascade_backrefs=False,
back_populates="asset",
)
owners = association_proxy(
"_owners",
"user",
creator=lambda user: AssetOwner(user=user),
)
def __str__(self):
return self.asset_name or ""
@ -225,6 +238,8 @@ def add_asset_proxies(subclass):
Asset.make_proxy(subclass, "asset", "thumbnail_url")
Asset.make_proxy(subclass, "asset", "image_url")
Asset.make_proxy(subclass, "asset", "archived")
Asset.make_proxy(subclass, "asset", "parents")
Asset.make_proxy(subclass, "asset", "owners")
class EggMixin:
@ -262,3 +277,27 @@ class AssetParent(model.Base):
Asset,
foreign_keys=parent_uuid,
)
class AssetOwner(model.Base):
"""
Represents a "asset's owner relationship" from farmOS.
"""
__tablename__ = "asset_owner"
__versioned__ = {}
uuid = model.uuid_column()
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
asset = orm.relationship(
Asset,
foreign_keys=asset_uuid,
back_populates="_owners",
)
user_uuid = model.uuid_fk_column("user.uuid", nullable=False)
user = orm.relationship(
model.User,
foreign_keys=user_uuid,
)

View file

@ -187,6 +187,7 @@ class AssetImporterBase(FromFarmOS, ToWutta):
fields.extend(
[
"parents",
"owners",
]
)
return fields
@ -194,8 +195,9 @@ class AssetImporterBase(FromFarmOS, ToWutta):
def get_source_objects(self):
""" """
asset_type = self.get_farmos_asset_type()
result = self.farmos_client.asset.get(asset_type)
return result["data"]
return list(
self.farmos_client.asset.iterate(asset_type, params={"include": "image"})
)
def normalize_source_data(self, **kwargs):
""" """
@ -208,10 +210,16 @@ class AssetImporterBase(FromFarmOS, ToWutta):
return data
def normalize_asset(self, asset):
def normalize_source_object(self, asset):
""" """
image_url = None
thumbnail_url = None
data = self.normal.normalize_farmos_asset(asset)
data["farmos_uuid"] = UUID(data.pop("uuid"))
data["asset_type"] = self.get_asset_type(asset)
if "image_url" in self.fields or "thumbnail_url" in self.fields:
data["image_url"] = None
data["thumbnail_url"] = None
if relationships := asset.get("relationships"):
if image := relationships.get("image"):
@ -222,35 +230,20 @@ class AssetImporterBase(FromFarmOS, ToWutta):
if image_style := image["data"]["attributes"].get(
"image_style_uri"
):
image_url = image_style["large"]
thumbnail_url = image_style["thumbnail"]
data["image_url"] = image_style["large"]
data["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 = []
data["parents"] = []
for parent in asset["relationships"]["parent"]["data"]:
parents.append((self.get_asset_type(parent), UUID(parent["id"])))
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,
}
if "owners" in self.fields:
data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]]
return data
def get_asset_type(self, asset):
return asset["type"].split("--")[1]
@ -259,10 +252,10 @@ class AssetImporterBase(FromFarmOS, ToWutta):
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
]
data["parents"] = [(p.asset_type, p.farmos_uuid) for p in asset.parents]
if "owners" in self.fields:
data["owners"] = [user.farmos_uuid for user in asset.owners]
return data
@ -303,6 +296,30 @@ class AssetImporterBase(FromFarmOS, ToWutta):
)
self.target_session.delete(parent)
if "owners" in self.fields:
if not target_data or target_data["owners"] != source_data["owners"]:
for farmos_uuid in source_data["owners"]:
if not target_data or farmos_uuid not in target_data["owners"]:
user = (
self.target_session.query(model.User)
.join(model.WuttaFarmUser)
.filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid)
.one()
)
asset.owners.append(user)
if target_data:
for farmos_uuid in target_data["owners"]:
if farmos_uuid not in source_data["owners"]:
user = (
self.target_session.query(model.User)
.join(model.WuttaFarmUser)
.filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid)
.one()
)
asset.owners.remove(user)
return asset
@ -338,11 +355,6 @@ class AnimalAssetImporter(AssetImporterBase):
if animal_type.farmos_uuid:
self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type
def get_source_objects(self):
""" """
animals = self.farmos_client.asset.get("animal")
return animals["data"]
def normalize_source_object(self, animal):
""" """
animal_type_uuid = None
@ -374,10 +386,9 @@ class AnimalAssetImporter(AssetImporterBase):
else:
sterile = animal["attributes"]["is_castrated"]
data = self.normalize_asset(animal)
data = super().normalize_source_object(animal)
data.update(
{
"asset_type": "animal",
"animal_type_uuid": animal_type_uuid,
"sex": animal["attributes"]["sex"],
"is_sterile": sterile,
@ -468,17 +479,11 @@ class GroupAssetImporter(AssetImporterBase):
"parents",
]
def get_source_objects(self):
""" """
groups = self.farmos_client.asset.get("group")
return groups["data"]
def normalize_source_object(self, group):
""" """
data = self.normalize_asset(group)
data = super().normalize_source_object(group)
data.update(
{
"asset_type": "group",
"produces_eggs": group["attributes"]["produces_eggs"],
}
)
@ -514,11 +519,6 @@ class LandAssetImporter(AssetImporterBase):
for land_type in self.target_session.query(model.LandType):
self.land_types_by_id[land_type.drupal_id] = land_type
def get_source_objects(self):
""" """
land_assets = self.farmos_client.asset.get("land")
return land_assets["data"]
def normalize_source_object(self, land):
""" """
land_type_id = land["attributes"]["land_type"]
@ -529,10 +529,9 @@ class LandAssetImporter(AssetImporterBase):
)
return None
data = self.normalize_asset(land)
data = super().normalize_source_object(land)
data.update(
{
"asset_type": "land",
"land_type_uuid": land_type.uuid,
}
)
@ -638,10 +637,9 @@ class PlantAssetImporter(AssetImporterBase):
else:
log.warning("plant type not found: %s", plant_type["id"])
data = self.normalize_asset(plant)
data = super().normalize_source_object(plant)
data.update(
{
"asset_type": "plant",
"plant_types": set(plant_types),
}
)
@ -718,11 +716,6 @@ class StructureAssetImporter(AssetImporterBase):
for structure_type in self.target_session.query(model.StructureType):
self.structure_types_by_id[structure_type.drupal_id] = structure_type
def get_source_objects(self):
""" """
structures = self.farmos_client.asset.get("structure")
return structures["data"]
def normalize_source_object(self, structure):
""" """
structure_type_id = structure["attributes"]["structure_type"]
@ -735,10 +728,9 @@ class StructureAssetImporter(AssetImporterBase):
)
return None
data = self.normalize_asset(structure)
data = super().normalize_source_object(structure)
data.update(
{
"asset_type": "structure",
"structure_type_uuid": structure_type.uuid,
}
)
@ -1167,7 +1159,7 @@ class LogImporterBase(FromFarmOS, ToWutta):
if not target_data or target_data["owners"] != source_data["owners"]:
for farmos_uuid in source_data["owners"]:
if not target_data or farmos_uuid not in target_data["assets"]:
if not target_data or farmos_uuid not in target_data["owners"]:
user = (
self.target_session.query(model.User)
.join(model.WuttaFarmUser)

View file

@ -84,6 +84,40 @@ class Normalizer(GenericHandler):
self._farmos_units = units
return self._farmos_units
def normalize_farmos_asset(self, asset, included={}):
""" """
if notes := asset["attributes"]["notes"]:
notes = notes["value"]
owner_objects = []
owner_uuids = []
if relationships := asset.get("relationships"):
if owners := relationships.get("owner"):
for user in owners["data"]:
user_uuid = user["id"]
owner_uuids.append(user_uuid)
if user := included.get(user_uuid):
owner_objects.append(
{
"uuid": user["id"],
"name": user["attributes"]["name"],
}
)
return {
"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": asset["attributes"]["archived"],
"notes": notes,
"owners": owner_objects,
"owner_uuids": owner_uuids,
}
def normalize_farmos_log(self, log, included={}):
if timestamp := log["attributes"]["timestamp"]:

View file

@ -228,6 +228,9 @@ class AnimalAssetView(AssetMasterView):
"birthdate",
"is_sterile",
"sex",
"group_membership",
"owners",
"locations",
"archived",
]

View file

@ -136,6 +136,10 @@ class AssetMasterView(WuttaFarmMasterView):
# parents
g.set_renderer("parents", self.render_parents_for_grid)
# owners
g.set_label("owners", "Owner")
g.set_renderer("owners", self.render_owners_for_grid)
# archived
g.set_renderer("archived", "boolean")
g.set_sorter("archived", model.Asset.archived)
@ -155,6 +159,17 @@ class AssetMasterView(WuttaFarmMasterView):
parents = [str(p.parent) for p in asset.parents]
return ", ".join(parents)
def render_owners_for_grid(self, asset, field, value):
if self.farmos_style_grid_links:
links = []
for user in asset.owners:
url = self.request.route_url("users.view", uuid=user.uuid)
links.append(tags.link_to(user.username, url))
return ", ".join(links)
return ", ".join([user.username for user in asset.owners])
def grid_row_class(self, asset, data, i):
""" """
if asset.archived:
@ -314,8 +329,11 @@ class AllAssetView(AssetMasterView):
"thumbnail",
"drupal_id",
"asset_name",
"group_membership",
"asset_type",
"parents",
"owners",
"locations",
"archived",
]

View file

@ -160,6 +160,7 @@ class StructureAssetView(AssetMasterView):
"asset_name",
"structure_type",
"parents",
"owners",
"archived",
]