feat: expose "current location" for assets
based on most recent movement log, as in farmOS
This commit is contained in:
parent
41870ee2e2
commit
759eb906b9
8 changed files with 124 additions and 19 deletions
|
|
@ -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
49
src/wuttafarm/assets.py
Normal 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 []
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ class AnimalAssetView(AssetMasterView):
|
||||||
farmos_bundle = "animal"
|
farmos_bundle = "animal"
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"animal_type": "Species/Breed",
|
"animal_type": "Species / Breed",
|
||||||
"is_sterile": "Sterile",
|
"is_sterile": "Sterile",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue