feat: use 'include' API param for better Animal Assets grid data

this commit also renames all farmOS asset routes, for some reason.  at
least now they are consistent
This commit is contained in:
Lance Edgar 2026-02-20 19:02:36 -06:00
parent bbb1207b27
commit 1af2b695dc
10 changed files with 188 additions and 71 deletions

View file

@ -189,7 +189,7 @@ class StructureWidget(Widget):
return tags.link_to( return tags.link_to(
structure["name"], structure["name"],
self.request.route_url( self.request.route_url(
"farmos_structures.view", uuid=structure["uuid"] "farmos_structure_assets.view", uuid=structure["uuid"]
), ),
) )

View file

@ -135,12 +135,20 @@ class SimpleSorter:
class ResourceData: class ResourceData:
def __init__(self, config, farmos_client, content_type, normalizer=None): def __init__(
self,
config,
farmos_client,
content_type,
include=None,
normalizer=None,
):
self.config = config self.config = config
self.farmos_client = farmos_client self.farmos_client = farmos_client
self.entity, self.bundle = content_type.split("--") self.entity, self.bundle = content_type.split("--")
self.filters = [] self.filters = []
self.sorters = [] self.sorters = []
self.include = include
self.normalizer = normalizer self.normalizer = normalizer
self._data = None self._data = None
@ -189,12 +197,17 @@ class ResourceData:
# params["page[offset]"] = start # params["page[offset]"] = start
# params["page[limit]"] = stop - start # params["page[limit]"] = stop - start
if self.include:
params["include"] = self.include
result = self.farmos_client.resource.get( result = self.farmos_client.resource.get(
self.entity, self.bundle, params=params self.entity, self.bundle, params=params
) )
data = result["data"] data = result["data"]
included = {obj["id"]: obj for obj in result.get("included", [])}
if self.normalizer: if self.normalizer:
data = [self.normalizer(d) for d in data] data = [self.normalizer(d, included) for d in data]
self._data = data self._data = data
return self._data return self._data

View file

@ -202,13 +202,13 @@ class WuttaFarmMenuHandler(base.MenuHandler):
{"type": "sep"}, {"type": "sep"},
{ {
"title": "Animal Assets", "title": "Animal Assets",
"route": "farmos_assets_animal", "route": "farmos_animal_assets",
"perm": "farmos_assets_animal.list", "perm": "farmos_animal_assets.list",
}, },
{ {
"title": "Group Assets", "title": "Group Assets",
"route": "farmos_groups", "route": "farmos_group_assets",
"perm": "farmos_groups.list", "perm": "farmos_group_assets.list",
}, },
{ {
"title": "Land Assets", "title": "Land Assets",
@ -217,13 +217,13 @@ class WuttaFarmMenuHandler(base.MenuHandler):
}, },
{ {
"title": "Plant Assets", "title": "Plant Assets",
"route": "farmos_asset_plant", "route": "farmos_plant_assets",
"perm": "farmos_asset_plant.list", "perm": "farmos_plant_assets.list",
}, },
{ {
"title": "Structure Assets", "title": "Structure Assets",
"route": "farmos_structures", "route": "farmos_structure_assets",
"perm": "farmos_structures.list", "perm": "farmos_structure_assets.list",
}, },
{"type": "sep"}, {"type": "sep"},
{ {
@ -311,13 +311,13 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"items": [ "items": [
{ {
"title": "Animal", "title": "Animal",
"route": "farmos_assets_animal", "route": "farmos_animal_assets",
"perm": "farmos_assets_animal.list", "perm": "farmos_animal_assets.list",
}, },
{ {
"title": "Group", "title": "Group",
"route": "farmos_groups", "route": "farmos_group_assets",
"perm": "farmos_groups.list", "perm": "farmos_group_assets.list",
}, },
{ {
"title": "Land", "title": "Land",
@ -326,13 +326,13 @@ class WuttaFarmMenuHandler(base.MenuHandler):
}, },
{ {
"title": "Plant", "title": "Plant",
"route": "farmos_asset_plant", "route": "farmos_plant_assets",
"perm": "farmos_asset_plant.list", "perm": "farmos_plant_assets.list",
}, },
{ {
"title": "Structure", "title": "Structure",
"route": "farmos_structures", "route": "farmos_structure_assets",
"perm": "farmos_structures.list", "perm": "farmos_structure_assets.list",
}, },
{"type": "sep"}, {"type": "sep"},
{ {

View file

@ -278,27 +278,12 @@ class AssetMasterView(WuttaFarmMasterView):
buttons = super().get_xref_buttons(asset) buttons = super().get_xref_buttons(asset)
if asset.farmos_uuid: if asset.farmos_uuid:
asset_type = self.get_model_class().__wutta_hint__["farmos_asset_type"]
# TODO route = f"farmos_{asset_type}_assets.view"
route = None url = self.request.route_url(route, uuid=asset.farmos_uuid)
if asset.asset_type == "animal":
route = "farmos_assets_animal.view"
elif asset.asset_type == "group":
route = "farmos_groups.view"
elif asset.asset_type == "land":
route = "farmos_land_assets.view"
elif asset.asset_type == "plant":
route = "farmos_asset_plant.view"
elif asset.asset_type == "structure":
route = "farmos_structures.view"
if route:
buttons.append( buttons.append(
self.make_button( self.make_button(
"View farmOS record", "View farmOS record", primary=True, url=url, icon_left="eye"
primary=True,
url=self.request.route_url(route, uuid=asset.farmos_uuid),
icon_left="eye",
) )
) )

View file

@ -65,14 +65,14 @@ class CommonView(base.CommonView):
"asset_types.list", "asset_types.list",
"asset_types.view", "asset_types.view",
"asset_types.versions", "asset_types.versions",
"farmos_animal_assets.list",
"farmos_animal_assets.view",
"farmos_animal_types.list", "farmos_animal_types.list",
"farmos_animal_types.view", "farmos_animal_types.view",
"farmos_assets_animal.list",
"farmos_assets_animal.view",
"farmos_asset_types.list", "farmos_asset_types.list",
"farmos_asset_types.view", "farmos_asset_types.view",
"farmos_groups.list", "farmos_group_assets.list",
"farmos_groups.view", "farmos_group_assets.view",
"farmos_land_assets.list", "farmos_land_assets.list",
"farmos_land_assets.view", "farmos_land_assets.view",
"farmos_land_types.list", "farmos_land_types.list",
@ -87,10 +87,10 @@ class CommonView(base.CommonView):
"farmos_logs_medical.view", "farmos_logs_medical.view",
"farmos_logs_observation.list", "farmos_logs_observation.list",
"farmos_logs_observation.view", "farmos_logs_observation.view",
"farmos_structure_assets.list",
"farmos_structure_assets.view",
"farmos_structure_types.list", "farmos_structure_types.list",
"farmos_structure_types.view", "farmos_structure_types.view",
"farmos_structures.list",
"farmos_structures.view",
"farmos_users.list", "farmos_users.list",
"farmos_users.view", "farmos_users.view",
"group_assets.create", "group_assets.create",

View file

@ -26,6 +26,7 @@ Master view for Farm Animals
import datetime import datetime
import colander import colander
from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDateTime from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget from wuttaweb.forms.widgets import WuttaDateTimeWidget
@ -45,11 +46,11 @@ class AnimalView(AssetMasterView):
Master view for Farm Animals Master view for Farm Animals
""" """
model_name = "farmos_animal_asset" model_name = "farmos_animal_assets"
model_title = "farmOS Animal Asset" model_title = "farmOS Animal Asset"
model_title_plural = "farmOS Animal Assets" model_title_plural = "farmOS Animal Assets"
route_prefix = "farmos_assets_animal" route_prefix = "farmos_animal_assets"
url_prefix = "/farmOS/assets/animal" url_prefix = "/farmOS/assets/animal"
farmos_asset_type = "animal" farmos_asset_type = "animal"
@ -57,6 +58,7 @@ class AnimalView(AssetMasterView):
labels = { labels = {
"animal_type": "Species / Breed", "animal_type": "Species / Breed",
"animal_type_name": "Species / Breed",
"is_sterile": "Sterile", "is_sterile": "Sterile",
} }
@ -64,9 +66,13 @@ class AnimalView(AssetMasterView):
"drupal_id", "drupal_id",
"name", "name",
"produces_eggs", "produces_eggs",
"animal_type_name",
"birthdate", "birthdate",
"is_sterile", "is_sterile",
"sex", "sex",
"groups",
"owners",
"locations",
"archived", "archived",
] ]
@ -78,6 +84,7 @@ class AnimalView(AssetMasterView):
"sex", "sex",
"is_sterile", "is_sterile",
"archived", "archived",
"groups",
"owners", "owners",
"location", "location",
"notes", "notes",
@ -87,6 +94,10 @@ class AnimalView(AssetMasterView):
"image", "image",
] ]
def get_grid_data(self, **kwargs):
kwargs.setdefault("include", "animal_type,group,owner,location")
return super().get_grid_data(**kwargs)
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
super().configure_grid(g) super().configure_grid(g)
@ -97,6 +108,11 @@ class AnimalView(AssetMasterView):
g.set_sorter("produces_eggs", SimpleSorter("produces_eggs")) g.set_sorter("produces_eggs", SimpleSorter("produces_eggs"))
g.set_filter("produces_eggs", NullableBooleanFilter) g.set_filter("produces_eggs", NullableBooleanFilter)
# animal_type_name
g.set_renderer("animal_type_name", self.render_animal_type_for_grid)
g.set_sorter("animal_type_name", SimpleSorter("animal_type.name"))
g.set_filter("animal_type_name", StringFilter, path="animal_type.name")
# birthdate # birthdate
g.set_renderer("birthdate", "date") g.set_renderer("birthdate", "date")
g.set_sorter("birthdate", SimpleSorter("birthdate")) g.set_sorter("birthdate", SimpleSorter("birthdate"))
@ -106,11 +122,27 @@ class AnimalView(AssetMasterView):
g.set_sorter("sex", SimpleSorter("sex")) g.set_sorter("sex", SimpleSorter("sex"))
g.set_filter("sex", StringFilter) g.set_filter("sex", StringFilter)
# groups
g.set_label("groups", "Group Membership")
g.set_renderer("groups", self.render_groups_for_grid)
# is_sterile # is_sterile
g.set_renderer("is_sterile", "boolean") g.set_renderer("is_sterile", "boolean")
g.set_sorter("is_sterile", SimpleSorter("is_sterile")) g.set_sorter("is_sterile", SimpleSorter("is_sterile"))
g.set_filter("is_sterile", BooleanFilter) g.set_filter("is_sterile", BooleanFilter)
def render_animal_type_for_grid(self, animal, field, value):
uuid = animal["animal_type"]["uuid"]
url = self.request.route_url("farmos_animal_types.view", uuid=uuid)
return tags.link_to(value, url)
def render_groups_for_grid(self, animal, field, value):
links = []
for group in animal["group_objects"]:
url = self.request.route_url("farmos_group_assets.view", uuid=group["uuid"])
links.append(tags.link_to(group["name"], url))
return ", ".join(links)
def get_instance(self): def get_instance(self):
data = super().get_instance() data = super().get_instance()
@ -118,6 +150,7 @@ class AnimalView(AssetMasterView):
if relationships := self.raw_json["data"].get("relationships"): if relationships := self.raw_json["data"].get("relationships"):
# add animal type # add animal type
if not data.get("animal_type"):
if animal_type := relationships.get("animal_type"): if animal_type := relationships.get("animal_type"):
if animal_type["data"]: if animal_type["data"]:
animal_type = self.farmos_client.resource.get_id( animal_type = self.farmos_client.resource.get_id(
@ -130,9 +163,8 @@ class AnimalView(AssetMasterView):
return data return data
def normalize_asset(self, animal): def normalize_asset(self, animal, included):
normal = super().normalize_asset(animal, included)
normal = super().normalize_asset(animal)
birthdate = animal["attributes"]["birthdate"] birthdate = animal["attributes"]["birthdate"]
if birthdate: if birthdate:
@ -145,8 +177,36 @@ class AnimalView(AssetMasterView):
else: else:
sterile = animal["attributes"]["is_castrated"] sterile = animal["attributes"]["is_castrated"]
animal_type = None
animal_type_name = None
group_objects = []
group_names = []
if relationships := animal.get("relationships"):
if animal_type := relationships.get("animal_type"):
if animal_type := included.get(animal_type["data"]["id"]):
animal_type = {
"uuid": animal_type["id"],
"name": animal_type["attributes"]["name"],
}
animal_type_name = animal_type["name"]
if groups := relationships.get("group"):
for group in groups["data"]:
if group := included.get(group["id"]):
group = {
"uuid": group["id"],
"name": group["attributes"]["name"],
}
group_objects.append(group)
group_names.append(group["name"])
normal.update( normal.update(
{ {
"animal_type": animal_type,
"animal_type_name": animal_type_name,
"group_objects": group_objects,
"group_names": group_names,
"birthdate": birthdate, "birthdate": birthdate,
"sex": animal["attributes"]["sex"] or colander.null, "sex": animal["attributes"]["sex"] or colander.null,
"is_sterile": sterile, "is_sterile": sterile,

View file

@ -24,6 +24,7 @@ Base class for Asset master views
""" """
import colander import colander
from webhelpers2.html import tags
from wuttafarm.web.views.farmos import FarmOSMasterView from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import UsersType, StructureType from wuttafarm.web.forms.schema import UsersType, StructureType
@ -48,12 +49,15 @@ class AssetMasterView(FarmOSMasterView):
labels = { labels = {
"name": "Asset Name", "name": "Asset Name",
"location": "Current Location", "owners": "Owner",
"locations": "Location",
} }
grid_columns = [ grid_columns = [
"drupal_id", "drupal_id",
"name", "name",
"owners",
"locations",
"archived", "archived",
] ]
@ -64,12 +68,14 @@ class AssetMasterView(FarmOSMasterView):
"archived": {"active": True, "verb": "is_false"}, "archived": {"active": True, "verb": "is_false"},
} }
def get_grid_data(self, columns=None, session=None): def get_grid_data(self, columns=None, session=None, **kwargs):
kwargs.setdefault("include", "owner,location")
kwargs.setdefault("normalizer", self.normalize_asset)
return ResourceData( return ResourceData(
self.config, self.config,
self.farmos_client, self.farmos_client,
f"asset--{self.farmos_asset_type}", f"asset--{self.farmos_asset_type}",
normalizer=self.normalize_asset, **kwargs,
) )
def configure_grid(self, grid): def configure_grid(self, grid):
@ -86,11 +92,33 @@ class AssetMasterView(FarmOSMasterView):
g.set_sorter("name", SimpleSorter("name")) g.set_sorter("name", SimpleSorter("name"))
g.set_filter("name", StringFilter) g.set_filter("name", StringFilter)
# owners
g.set_renderer("owners", self.render_owners_for_grid)
# locations
g.set_renderer("locations", self.render_locations_for_grid)
# archived # archived
g.set_renderer("archived", "boolean") g.set_renderer("archived", "boolean")
g.set_sorter("archived", SimpleSorter("archived")) g.set_sorter("archived", SimpleSorter("archived"))
g.set_filter("archived", BooleanFilter) g.set_filter("archived", BooleanFilter)
def render_owners_for_grid(self, asset, field, value):
links = []
for user in value:
url = self.request.route_url("farmos_users.view", uuid=user["uuid"])
links.append(tags.link_to(user["name"], url))
return ", ".join(links)
def render_locations_for_grid(self, asset, field, value):
links = []
for location in value:
asset_type = location["type"].split("--")[1]
route = f"farmos_{asset_type}_assets.view"
url = self.request.route_url(route, uuid=location["uuid"])
links.append(tags.link_to(location["name"], url))
return ", ".join(links)
def grid_row_class(self, asset, data, i): def grid_row_class(self, asset, data, i):
""" """ """ """
if asset["archived"]: if asset["archived"]:
@ -104,7 +132,7 @@ class AssetMasterView(FarmOSMasterView):
self.raw_json = asset self.raw_json = asset
# instance data # instance data
data = self.normalize_asset(asset["data"]) data = self.normalize_asset(asset["data"], {})
if relationships := asset["data"].get("relationships"): if relationships := asset["data"].get("relationships"):
@ -155,7 +183,7 @@ class AssetMasterView(FarmOSMasterView):
def get_instance_title(self, asset): def get_instance_title(self, asset):
return asset["name"] return asset["name"]
def normalize_asset(self, asset): def normalize_asset(self, asset, included):
if notes := asset["attributes"]["notes"]: if notes := asset["attributes"]["notes"]:
notes = notes["value"] notes = notes["value"]
@ -165,12 +193,43 @@ class AssetMasterView(FarmOSMasterView):
else: else:
archived = asset["attributes"]["status"] == "archived" archived = asset["attributes"]["status"] == "archived"
owner_objects = []
owner_names = []
location_objects = []
location_names = []
if relationships := asset.get("relationships"):
if owners := relationships.get("owner"):
for user in owners["data"]:
if user := included.get(user["id"]):
user = {
"uuid": user["id"],
"name": user["attributes"]["name"],
}
owner_objects.append(user)
owner_names.append(user["name"])
if locations := relationships.get("location"):
for location in locations["data"]:
if location := included.get(location["id"]):
location = {
"uuid": location["id"],
"type": location["type"],
"name": location["attributes"]["name"],
}
location_objects.append(location)
location_names.append(location["name"])
return { return {
"uuid": asset["id"], "uuid": asset["id"],
"drupal_id": asset["attributes"]["drupal_internal__id"], "drupal_id": asset["attributes"]["drupal_internal__id"],
"name": asset["attributes"]["name"], "name": asset["attributes"]["name"],
"location": colander.null, # TODO "location": colander.null, # TODO
"notes": notes or colander.null, "notes": notes or colander.null,
"owners": owner_objects,
"owner_names": owner_names,
"locations": location_objects,
"location_names": location_names,
"archived": archived, "archived": archived,
} }

View file

@ -41,7 +41,7 @@ class GroupView(FarmOSMasterView):
model_title = "farmOS Group" model_title = "farmOS Group"
model_title_plural = "farmOS Groups" model_title_plural = "farmOS Groups"
route_prefix = "farmos_groups" route_prefix = "farmos_group_assets"
url_prefix = "/farmOS/groups" url_prefix = "/farmOS/groups"
farmos_refurl_path = "/assets/group" farmos_refurl_path = "/assets/group"

View file

@ -80,11 +80,11 @@ class PlantAssetView(FarmOSMasterView):
Master view for farmOS Plant Assets Master view for farmOS Plant Assets
""" """
model_name = "farmos_asset_plant" model_name = "farmos_plant_assets"
model_title = "farmOS Plant Asset" model_title = "farmOS Plant Asset"
model_title_plural = "farmOS Plant Assets" model_title_plural = "farmOS Plant Assets"
route_prefix = "farmos_asset_plant" route_prefix = "farmos_plant_assets"
url_prefix = "/farmOS/assets/plant" url_prefix = "/farmOS/assets/plant"
farmos_refurl_path = "/assets/plant" farmos_refurl_path = "/assets/plant"

View file

@ -39,11 +39,11 @@ class StructureView(FarmOSMasterView):
View for farmOS Structures View for farmOS Structures
""" """
model_name = "farmos_structure" model_name = "farmos_structure_asset"
model_title = "farmOS Structure" model_title = "farmOS Structure"
model_title_plural = "farmOS Structures" model_title_plural = "farmOS Structures"
route_prefix = "farmos_structures" route_prefix = "farmos_structure_assets"
url_prefix = "/farmOS/structures" url_prefix = "/farmOS/structures"
farmos_refurl_path = "/assets/structure" farmos_refurl_path = "/assets/structure"