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) 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): class UsersType(colander.SchemaType):
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):

View file

@ -30,13 +30,14 @@ from deform.widget import Widget
from webhelpers2.html import HTML, tags 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 def __init__(self, alt_text, *args, **kwargs):
types of images need to be supported. super().__init__(*args, **kwargs)
""" self.alt_text = alt_text
def serialize(self, field, cstruct, **kw): def serialize(self, field, cstruct, **kw):
readonly = kw.get("readonly", self.readonly) readonly = kw.get("readonly", self.readonly)
@ -44,7 +45,7 @@ class AnimalImage(Widget):
if cstruct in (colander.null, None): if cstruct in (colander.null, None):
return HTML.tag("span") 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) return super().serialize(field, cstruct, **kw)
@ -75,6 +76,32 @@ class AnimalTypeWidget(Widget):
return super().serialize(field, cstruct, **kw) 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): class UsersWidget(Widget):
""" """
Widget to display the list of owners for an asset etc. 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", "route": "farmos_animal_types",
"perm": "farmos_animal_types.list", "perm": "farmos_animal_types.list",
}, },
{
"title": "Structures",
"route": "farmos_structures",
"perm": "farmos_structures.list",
},
{"type": "sep"}, {"type": "sep"},
{ {
"title": "Users", "title": "Users",

View file

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

View file

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

View file

@ -45,6 +45,12 @@ class FarmOSMasterView(MasterView):
farmos_refurl_path = None 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): def __init__(self, request, context=None):
super().__init__(request, context=context) super().__init__(request, context=context)
self.farmos_client = self.get_farmos_client() 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)