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(
structure["name"],
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:
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.farmos_client = farmos_client
self.entity, self.bundle = content_type.split("--")
self.filters = []
self.sorters = []
self.include = include
self.normalizer = normalizer
self._data = None
@ -189,12 +197,17 @@ class ResourceData:
# params["page[offset]"] = start
# params["page[limit]"] = stop - start
if self.include:
params["include"] = self.include
result = self.farmos_client.resource.get(
self.entity, self.bundle, params=params
)
data = result["data"]
included = {obj["id"]: obj for obj in result.get("included", [])}
if self.normalizer:
data = [self.normalizer(d) for d in data]
data = [self.normalizer(d, included) for d in data]
self._data = data
return self._data

View file

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

View file

@ -278,27 +278,12 @@ class AssetMasterView(WuttaFarmMasterView):
buttons = super().get_xref_buttons(asset)
if asset.farmos_uuid:
# TODO
route = None
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:
asset_type = self.get_model_class().__wutta_hint__["farmos_asset_type"]
route = f"farmos_{asset_type}_assets.view"
url = self.request.route_url(route, uuid=asset.farmos_uuid)
buttons.append(
self.make_button(
"View farmOS record",
primary=True,
url=self.request.route_url(route, uuid=asset.farmos_uuid),
icon_left="eye",
"View farmOS record", primary=True, url=url, icon_left="eye"
)
)

View file

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

View file

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

View file

@ -24,6 +24,7 @@ Base class for Asset master views
"""
import colander
from webhelpers2.html import tags
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import UsersType, StructureType
@ -48,12 +49,15 @@ class AssetMasterView(FarmOSMasterView):
labels = {
"name": "Asset Name",
"location": "Current Location",
"owners": "Owner",
"locations": "Location",
}
grid_columns = [
"drupal_id",
"name",
"owners",
"locations",
"archived",
]
@ -64,12 +68,14 @@ class AssetMasterView(FarmOSMasterView):
"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(
self.config,
self.farmos_client,
f"asset--{self.farmos_asset_type}",
normalizer=self.normalize_asset,
**kwargs,
)
def configure_grid(self, grid):
@ -86,11 +92,33 @@ class AssetMasterView(FarmOSMasterView):
g.set_sorter("name", SimpleSorter("name"))
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
g.set_renderer("archived", "boolean")
g.set_sorter("archived", SimpleSorter("archived"))
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):
""" """
if asset["archived"]:
@ -104,7 +132,7 @@ class AssetMasterView(FarmOSMasterView):
self.raw_json = asset
# instance data
data = self.normalize_asset(asset["data"])
data = self.normalize_asset(asset["data"], {})
if relationships := asset["data"].get("relationships"):
@ -155,7 +183,7 @@ class AssetMasterView(FarmOSMasterView):
def get_instance_title(self, asset):
return asset["name"]
def normalize_asset(self, asset):
def normalize_asset(self, asset, included):
if notes := asset["attributes"]["notes"]:
notes = notes["value"]
@ -165,12 +193,43 @@ class AssetMasterView(FarmOSMasterView):
else:
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 {
"uuid": asset["id"],
"drupal_id": asset["attributes"]["drupal_internal__id"],
"name": asset["attributes"]["name"],
"location": colander.null, # TODO
"notes": notes or colander.null,
"owners": owner_objects,
"owner_names": owner_names,
"locations": location_objects,
"location_names": location_names,
"archived": archived,
}

View file

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

View file

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

View file

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