feat: add view for farmOS structures

This commit is contained in:
Lance Edgar 2026-02-07 16:42:32 -06:00
parent baacd1c15c
commit d9ef550100
8 changed files with 287 additions and 15 deletions

View file

@ -47,6 +47,25 @@ class AnimalTypeType(colander.SchemaType):
return AnimalTypeWidget(self.request, **kwargs)
class StructureType(colander.SchemaType):
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
return json.dumps(appstruct)
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
""" """
from wuttafarm.web.forms.widgets import StructureWidget
return StructureWidget(self.request, **kwargs)
class UsersType(colander.SchemaType):
def __init__(self, request, *args, **kwargs):

View file

@ -30,13 +30,14 @@ from deform.widget import Widget
from webhelpers2.html import HTML, tags
class AnimalImage(Widget):
class ImageWidget(Widget):
"""
Widget to display an image URL for a record.
"""
Widget to display an image URL for an animal.
TODO: this should be refactored to a more general name, once more
types of images need to be supported.
"""
def __init__(self, alt_text, *args, **kwargs):
super().__init__(*args, **kwargs)
self.alt_text = alt_text
def serialize(self, field, cstruct, **kw):
readonly = kw.get("readonly", self.readonly)
@ -44,7 +45,7 @@ class AnimalImage(Widget):
if cstruct in (colander.null, None):
return HTML.tag("span")
return tags.image(cstruct, "animal image", **kw)
return tags.image(cstruct, self.alt_text, **kw)
return super().serialize(field, cstruct, **kw)
@ -75,6 +76,32 @@ class AnimalTypeWidget(Widget):
return super().serialize(field, cstruct, **kw)
class StructureWidget(Widget):
"""
Widget to display a "structure" field.
"""
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
def serialize(self, field, cstruct, **kw):
readonly = kw.get("readonly", self.readonly)
if readonly:
if cstruct in (colander.null, None):
return HTML.tag("span")
structure = json.loads(cstruct)
return tags.link_to(
structure["name"],
self.request.route_url(
"farmos_structures.view", uuid=structure["uuid"]
),
)
return super().serialize(field, cstruct, **kw)
class UsersWidget(Widget):
"""
Widget to display the list of owners for an asset etc.

View file

@ -52,6 +52,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "farmos_animal_types",
"perm": "farmos_animal_types.list",
},
{
"title": "Structures",
"route": "farmos_structures",
"perm": "farmos_structures.list",
},
{"type": "sep"},
{
"title": "Users",

View file

@ -52,6 +52,8 @@ class CommonView(base.CommonView):
"farmos_animal_types.view",
"farmos_animals.list",
"farmos_animals.view",
"farmos_structures.list",
"farmos_structures.view",
"farmos_users.list",
"farmos_users.view",
]

View file

@ -28,5 +28,6 @@ from .master import FarmOSMasterView
def includeme(config):
config.include("wuttafarm.web.views.farmos.users")
config.include("wuttafarm.web.views.farmos.structures")
config.include("wuttafarm.web.views.farmos.animal_types")
config.include("wuttafarm.web.views.farmos.animals")

View file

@ -29,8 +29,8 @@ import colander
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import UsersType, AnimalTypeType
from wuttafarm.web.forms.widgets import AnimalImage
from wuttafarm.web.forms.schema import UsersType, AnimalTypeType, StructureType
from wuttafarm.web.forms.widgets import ImageWidget
class AnimalView(FarmOSMasterView):
@ -50,10 +50,7 @@ class AnimalView(FarmOSMasterView):
labels = {
"animal_type": "Species / Breed",
"is_castrated": "Castrated",
"location_name": "Current Location",
"raw_image_url": "Raw Image URL",
"large_image_url": "Large Image URL",
"thumbnail_image_url": "Thumbnail Image URL",
"location": "Current Location",
}
grid_columns = [
@ -74,7 +71,7 @@ class AnimalView(FarmOSMasterView):
"is_castrated",
"status",
"owners",
"location_name",
"location",
"notes",
"raw_image_url",
"large_image_url",
@ -125,7 +122,10 @@ class AnimalView(FarmOSMasterView):
location = self.farmos_client.resource.get_id(
"asset", "structure", location["data"][0]["id"]
)
data["location_name"] = location["data"]["attributes"]["name"]
data["location"] = {
"uuid": location["data"]["id"],
"name": location["data"]["attributes"]["name"],
}
# add owners
if owner := relationships.get("owner"):
@ -193,6 +193,9 @@ class AnimalView(FarmOSMasterView):
# is_castrated
f.set_node("is_castrated", colander.Boolean())
# location
f.set_node("location", StructureType(self.request))
# owners
f.set_node("owners", UsersType(self.request))
@ -201,7 +204,7 @@ class AnimalView(FarmOSMasterView):
# image
if url := animal.get("large_image_url"):
f.set_widget("image", AnimalImage())
f.set_widget("image", ImageWidget("animal image"))
f.set_default("image", url)
def get_xref_buttons(self, animal):

View file

@ -45,6 +45,12 @@ class FarmOSMasterView(MasterView):
farmos_refurl_path = None
labels = {
"raw_image_url": "Raw Image URL",
"large_image_url": "Large Image URL",
"thumbnail_image_url": "Thumbnail Image URL",
}
def __init__(self, request, context=None):
super().__init__(request, context=context)
self.farmos_client = self.get_farmos_client()

View file

@ -0,0 +1,209 @@
# -*- 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/>.
#
################################################################################
"""
View for farmOS Structures
"""
import datetime
import colander
from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.widgets import ImageWidget
class StructureView(FarmOSMasterView):
"""
View for farmOS Structures
"""
model_name = "farmos_structure"
model_title = "farmOS Structure"
model_title_plural = "farmOS Structures"
route_prefix = "farmos_structures"
url_prefix = "/farmOS/structures"
farmos_refurl_path = "/assets/structure"
grid_columns = [
"name",
"status",
"created",
"changed",
]
sort_defaults = "name"
form_fields = [
"name",
"status",
"structure_type",
"is_location",
"is_fixed",
"notes",
"created",
"changed",
"raw_image_url",
"large_image_url",
"thumbnail_image_url",
"image",
]
def get_grid_data(self, columns=None, session=None):
structures = self.farmos_client.resource.get("asset", "structure")
return [self.normalize_structure(s) for s in structures["data"]]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# name
g.set_link("name")
g.set_searchable("name")
# created
g.set_renderer("created", "datetime")
# changed
g.set_renderer("changed", "datetime")
def get_instance(self):
structure = self.farmos_client.resource.get_id(
"asset", "structure", self.request.matchdict["uuid"]
)
data = self.normalize_structure(structure["data"])
if relationships := structure["data"].get("relationships"):
# add owners
if owner := relationships.get("owner"):
data["owners"] = []
for owner_data in owner["data"]:
owner = self.farmos_client.resource.get_id(
"user", "user", owner_data["id"]
)
data["owners"].append(
{
"uuid": owner["data"]["id"],
"display_name": owner["data"]["attributes"]["display_name"],
}
)
# add image urls
if image := relationships.get("image"):
if image["data"]:
image = self.farmos_client.resource.get_id(
"file", "file", image["data"][0]["id"]
)
data["raw_image_url"] = self.app.get_farmos_url(
image["data"]["attributes"]["uri"]["url"]
)
# nb. other styles available: medium, wide
data["large_image_url"] = image["data"]["attributes"][
"image_style_uri"
]["large"]
data["thumbnail_image_url"] = image["data"]["attributes"][
"image_style_uri"
]["thumbnail"]
return data
def get_instance_title(self, structure):
return structure["name"]
def normalize_structure(self, structure):
if created := structure["attributes"].get("created"):
created = datetime.datetime.fromisoformat(created)
created = self.app.localtime(created)
if changed := structure["attributes"].get("changed"):
changed = datetime.datetime.fromisoformat(changed)
changed = self.app.localtime(changed)
return {
"uuid": structure["id"],
"drupal_internal_id": structure["attributes"]["drupal_internal__id"],
"name": structure["attributes"]["name"],
"structure_type": structure["attributes"]["structure_type"],
"is_fixed": structure["attributes"]["is_fixed"],
"is_location": structure["attributes"]["is_location"],
"notes": structure["attributes"]["notes"] or colander.null,
"status": structure["attributes"]["status"],
"created": created,
"changed": changed,
}
def configure_form(self, form):
f = form
super().configure_form(f)
structure = f.model_instance
# is_fixed
f.set_node("is_fixed", colander.Boolean())
# is_location
f.set_node("is_location", colander.Boolean())
# notes
f.set_widget("notes", "notes")
# created
f.set_node("created", WuttaDateTime())
f.set_widget("created", WuttaDateTimeWidget(self.request))
# changed
f.set_node("changed", WuttaDateTime())
f.set_widget("changed", WuttaDateTimeWidget(self.request))
# image
if url := structure.get("large_image_url"):
f.set_widget("image", ImageWidget("structure image"))
f.set_default("image", url)
def get_xref_buttons(self, structure):
drupal_id = structure["drupal_internal_id"]
return [
self.make_button(
"View in farmOS",
primary=True,
url=self.app.get_farmos_url(f"/asset/{drupal_id}"),
target="_blank",
icon_left="external-link-alt",
),
]
def defaults(config, **kwargs):
base = globals()
StructureView = kwargs.get("StructureView", base["StructureView"])
StructureView.defaults(config)
def includeme(config):
defaults(config)