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_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler" 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): def get_farmos_handler(self):
""" """
Get the configured farmOS integration handler. 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) return AssetParentRefsWidget(self.request, **kwargs)
class LogAssetRefs(WuttaSet): class AssetRefs(WuttaSet):
""" """
Schema type for Assets field (on a Log record) Schema type for Assets field (on a Log record)
""" """
@ -376,9 +376,9 @@ class LogAssetRefs(WuttaSet):
return {asset.uuid for asset in appstruct} return {asset.uuid for asset in appstruct}
def widget_maker(self, **kwargs): 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): class LogQuantityRefs(WuttaSet):
@ -398,7 +398,7 @@ class LogQuantityRefs(WuttaSet):
return LogQuantityRefsWidget(self.request, **kwargs) return LogQuantityRefsWidget(self.request, **kwargs)
class LogOwnerRefs(WuttaSet): class OwnerRefs(WuttaSet):
""" """
Schema type for Owners field (on a Log record) Schema type for Owners field (on a Log record)
""" """
@ -410,9 +410,9 @@ class LogOwnerRefs(WuttaSet):
return {user.uuid for user in appstruct} return {user.uuid for user in appstruct}
def widget_maker(self, **kwargs): 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): class Notes(colander.String):

View file

@ -423,9 +423,9 @@ class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
return super().serialize(field, cstruct, **kw) 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): def serialize(self, field, cstruct, **kw):
@ -486,9 +486,9 @@ class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget):
return super().serialize(field, cstruct, **kw) 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): def serialize(self, field, cstruct, **kw):

View file

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

View file

@ -32,7 +32,7 @@ from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Asset, Log 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.web.forms.widgets import ImageWidget
from wuttafarm.util import get_log_type_enum from wuttafarm.util import get_log_type_enum
from wuttafarm.web.util import get_farmos_client_for_user from wuttafarm.web.util import get_farmos_client_for_user
@ -140,6 +140,10 @@ class AssetMasterView(WuttaFarmMasterView):
g.set_label("owners", "Owner") g.set_label("owners", "Owner")
g.set_renderer("owners", self.render_owners_for_grid) 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 # archived
g.set_renderer("archived", "boolean") g.set_renderer("archived", "boolean")
g.set_sorter("archived", model.Asset.archived) g.set_sorter("archived", model.Asset.archived)
@ -170,6 +174,21 @@ class AssetMasterView(WuttaFarmMasterView):
return ", ".join([user.username for user in asset.owners]) 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): def grid_row_class(self, asset, data, i):
""" """ """ """
if asset.archived: if asset.archived:
@ -179,6 +198,7 @@ class AssetMasterView(WuttaFarmMasterView):
def configure_form(self, form): def configure_form(self, form):
f = form f = form
super().configure_form(f) super().configure_form(f)
asset_handler = self.app.get_asset_handler()
asset = form.model_instance asset = form.model_instance
# asset_type # asset_type
@ -191,12 +211,30 @@ class AssetMasterView(WuttaFarmMasterView):
) )
f.set_readonly("asset_type") 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 # parents
if self.creating or self.editing: if self.creating or self.editing:
f.remove("parents") # TODO: add support for this f.remove("parents") # TODO: add support for this
else: else:
f.set_node("parents", AssetParentRefs(self.request)) 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 # notes
f.set_widget("notes", "notes") f.set_widget("notes", "notes")

View file

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

View file

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