feat: expose "current location" for assets

based on most recent movement log, as in farmOS
This commit is contained in:
Lance Edgar 2026-03-02 20:59:27 -06:00
parent 41870ee2e2
commit 759eb906b9
8 changed files with 124 additions and 19 deletions

View file

@ -36,6 +36,21 @@ class WuttaFarmAppHandler(base.AppHandler):
default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler"
def get_asset_handler(self):
"""
Get the configured asset handler.
:rtype: :class:`~wuttafarm.assets.AssetHandler`
"""
if "asset" not in self.handlers:
spec = self.config.get(
f"{self.appname}.asset_handler",
default="wuttafarm.assets:AssetHandler",
)
factory = self.load_object(spec)
self.handlers["asset"] = factory(self.config)
return self.handlers["asset"]
def get_farmos_handler(self):
"""
Get the configured farmOS integration handler.

49
src/wuttafarm/assets.py Normal file
View file

@ -0,0 +1,49 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Asset handler
"""
from wuttjamaican.app import GenericHandler
class AssetHandler(GenericHandler):
"""
Base class and default implementation for the asset
:term:`handler`.
"""
def get_locations(self, asset):
model = self.app.model
session = self.app.get_session(asset)
loclog = (
session.query(model.Log)
.join(model.LogAsset)
.filter(model.LogAsset.asset == asset)
.filter(model.Log.is_movement == True)
.order_by(model.Log.timestamp.desc())
.first()
)
if loclog:
return loclog.locations
return []

View file

@ -364,7 +364,7 @@ class AssetParentRefs(WuttaSet):
return AssetParentRefsWidget(self.request, **kwargs)
class LogAssetRefs(WuttaSet):
class AssetRefs(WuttaSet):
"""
Schema type for Assets field (on a Log record)
"""
@ -376,9 +376,9 @@ class LogAssetRefs(WuttaSet):
return {asset.uuid for asset in appstruct}
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import LogAssetRefsWidget
from wuttafarm.web.forms.widgets import AssetRefsWidget
return LogAssetRefsWidget(self.request, **kwargs)
return AssetRefsWidget(self.request, **kwargs)
class LogQuantityRefs(WuttaSet):
@ -398,7 +398,7 @@ class LogQuantityRefs(WuttaSet):
return LogQuantityRefsWidget(self.request, **kwargs)
class LogOwnerRefs(WuttaSet):
class OwnerRefs(WuttaSet):
"""
Schema type for Owners field (on a Log record)
"""
@ -410,9 +410,9 @@ class LogOwnerRefs(WuttaSet):
return {user.uuid for user in appstruct}
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import LogOwnerRefsWidget
from wuttafarm.web.forms.widgets import OwnerRefsWidget
return LogOwnerRefsWidget(self.request, **kwargs)
return OwnerRefsWidget(self.request, **kwargs)
class Notes(colander.String):

View file

@ -423,9 +423,9 @@ class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
return super().serialize(field, cstruct, **kw)
class LogAssetRefsWidget(WuttaCheckboxChoiceWidget):
class AssetRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for Assets field (on a Log record)
Widget for Assets field (of various kinds).
"""
def serialize(self, field, cstruct, **kw):
@ -486,9 +486,9 @@ class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget):
return super().serialize(field, cstruct, **kw)
class LogOwnerRefsWidget(WuttaCheckboxChoiceWidget):
class OwnerRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for Owners field (on a Log record)
Widget for Owners field (on an Asset or Log record)
"""
def serialize(self, field, cstruct, **kw):

View file

@ -243,6 +243,8 @@ class AnimalAssetView(AssetMasterView):
"is_sterile",
"notes",
"asset_type",
"owners",
"locations",
"archived",
"drupal_id",
"farmos_uuid",

View file

@ -32,7 +32,7 @@ from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Asset, Log
from wuttafarm.web.forms.schema import AssetParentRefs
from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs
from wuttafarm.web.forms.widgets import ImageWidget
from wuttafarm.util import get_log_type_enum
from wuttafarm.web.util import get_farmos_client_for_user
@ -140,6 +140,10 @@ class AssetMasterView(WuttaFarmMasterView):
g.set_label("owners", "Owner")
g.set_renderer("owners", self.render_owners_for_grid)
# locations
g.set_label("locations", "Location")
g.set_renderer("locations", self.render_locations_for_grid)
# archived
g.set_renderer("archived", "boolean")
g.set_sorter("archived", model.Asset.archived)
@ -170,6 +174,21 @@ class AssetMasterView(WuttaFarmMasterView):
return ", ".join([user.username for user in asset.owners])
def render_locations_for_grid(self, asset, field, value):
asset_handler = self.app.get_asset_handler()
locations = asset_handler.get_locations(asset)
if self.farmos_style_grid_links:
links = []
for loc in locations:
url = self.request.route_url(
f"{loc.asset_type}_assets.view", uuid=loc.uuid
)
links.append(tags.link_to(str(loc), url))
return ", ".join(links)
return ", ".join([str(loc) for loc in locations])
def grid_row_class(self, asset, data, i):
""" """
if asset.archived:
@ -179,6 +198,7 @@ class AssetMasterView(WuttaFarmMasterView):
def configure_form(self, form):
f = form
super().configure_form(f)
asset_handler = self.app.get_asset_handler()
asset = form.model_instance
# asset_type
@ -191,12 +211,30 @@ class AssetMasterView(WuttaFarmMasterView):
)
f.set_readonly("asset_type")
# owners
if self.creating or self.editing:
f.remove("owners") # TODO: need to support this
else:
f.set_node("owners", OwnerRefs(self.request))
# nb. must explicity declare value for non-standard field
f.set_default("owners", asset.owners)
# locations
if self.creating or self.editing:
# nb. this is a calculated field
f.remove("locations")
else:
f.set_label("locations", "Current Location")
f.set_node("locations", AssetRefs(self.request))
# nb. must explicity declare value for non-standard field
f.set_default("locations", asset_handler.get_locations(asset))
# parents
if self.creating or self.editing:
f.remove("parents") # TODO: add support for this
else:
f.set_node("parents", AssetParentRefs(self.request))
f.set_default("parents", [p.parent_uuid for p in asset.asset._parents])
f.set_default("parents", [p.uuid for p in asset.parents])
# notes
f.set_widget("notes", "notes")

View file

@ -53,7 +53,6 @@ class AssetMasterView(FarmOSMasterView):
labels = {
"name": "Asset Name",
"asset_type_name": "Asset Type",
"owners": "Owner",
"locations": "Location",
"thumbnail_url": "Thumbnail URL",
"image_url": "Image URL",
@ -104,6 +103,7 @@ class AssetMasterView(FarmOSMasterView):
g.set_filter("name", StringFilter)
# owners
g.set_label("owners", "Owner")
g.set_renderer("owners", self.render_owners_for_grid)
# locations
@ -239,6 +239,7 @@ class AssetMasterView(FarmOSMasterView):
if self.creating or self.editing:
f.remove("locations")
else:
f.set_label("locations", "Current Location")
f.set_node("locations", FarmOSLocationRefs(self.request))
# owners

View file

@ -34,7 +34,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import LogType, Log
from wuttafarm.web.forms.schema import LogAssetRefs, LogQuantityRefs, LogOwnerRefs
from wuttafarm.web.forms.schema import AssetRefs, LogQuantityRefs, OwnerRefs
from wuttafarm.util import get_log_type_enum
@ -259,7 +259,7 @@ class LogMasterView(WuttaFarmMasterView):
if self.creating or self.editing:
f.remove("assets") # TODO: need to support this
else:
f.set_node("assets", LogAssetRefs(self.request))
f.set_node("assets", AssetRefs(self.request))
# nb. must explicity declare value for non-standard field
f.set_default("assets", log.assets)
@ -267,7 +267,7 @@ class LogMasterView(WuttaFarmMasterView):
if self.creating or self.editing:
f.remove("groups") # TODO: need to support this
else:
f.set_node("groups", LogAssetRefs(self.request))
f.set_node("groups", AssetRefs(self.request))
# nb. must explicity declare value for non-standard field
f.set_default("groups", log.groups)
@ -275,7 +275,7 @@ class LogMasterView(WuttaFarmMasterView):
if self.creating or self.editing:
f.remove("locations") # TODO: need to support this
else:
f.set_node("locations", LogAssetRefs(self.request))
f.set_node("locations", AssetRefs(self.request))
# nb. must explicity declare value for non-standard field
f.set_default("locations", log.locations)
@ -306,7 +306,7 @@ class LogMasterView(WuttaFarmMasterView):
if self.creating or self.editing:
f.remove("owners") # TODO: need to support this
else:
f.set_node("owners", LogOwnerRefs(self.request))
f.set_node("owners", OwnerRefs(self.request))
# nb. must explicity declare value for non-standard field
f.set_default("owners", log.owners)