From 74155049268d1bd454fe9689f5878548cdad6655 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 14:26:49 -0600 Subject: [PATCH 01/14] feat: add view for farmOS users --- src/wuttafarm/web/forms/schema.py | 47 +++++++ src/wuttafarm/web/forms/widgets.py | 30 +++++ src/wuttafarm/web/menus.py | 6 + src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/animals.py | 22 ++-- src/wuttafarm/web/views/farmos/users.py | 139 +++++++++++++++++++++ 7 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 src/wuttafarm/web/forms/schema.py create mode 100644 src/wuttafarm/web/views/farmos/users.py diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py new file mode 100644 index 0000000..132c7e3 --- /dev/null +++ b/src/wuttafarm/web/forms/schema.py @@ -0,0 +1,47 @@ +# -*- 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 . +# +################################################################################ +""" +Custom form widgets for WuttaFarm +""" + +import json + +import colander + + +class UsersType(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 UsersWidget + + return UsersWidget(self.request, **kwargs) diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py index 047a8cd..933e8a4 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -23,6 +23,8 @@ Custom form widgets for WuttaFarm """ +import json + import colander from deform.widget import Widget from webhelpers2.html import HTML, tags @@ -45,3 +47,31 @@ class AnimalImage(Widget): return tags.image(cstruct, "animal image", **kw) return super().serialize(field, cstruct, **kw) + + +class UsersWidget(Widget): + """ + Widget to display the list of owners for an asset etc. + """ + + 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") + + items = [] + for user in json.loads(cstruct): + link = tags.link_to( + user["display_name"], + self.request.route_url("farmos_users.view", uuid=user["uuid"]), + ) + items.append(HTML.tag("li", c=link)) + + return HTML.tag("ul", c=items) + + return super().serialize(field, cstruct, **kw) diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index e999944..642f427 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -47,5 +47,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_animals", "perm": "farmos_animals.list", }, + {"type": "sep"}, + { + "title": "Users", + "route": "farmos_users", + "perm": "farmos_users.list", + }, ], } diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index bb8a9c9..278d669 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -50,6 +50,8 @@ class CommonView(base.CommonView): site_admin_perms = [ "farmos_animals.list", "farmos_animals.view", + "farmos_users.list", + "farmos_users.view", ] for perm in site_admin_perms: auth.grant_permission(site_admin, perm) diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index df789b3..dd03b86 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -27,4 +27,5 @@ from .master import FarmOSMasterView def includeme(config): + config.include("wuttafarm.web.views.farmos.users") config.include("wuttafarm.web.views.farmos.animals") diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py index aa00412..b6228c7 100644 --- a/src/wuttafarm/web/views/farmos/animals.py +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -28,6 +28,8 @@ import datetime import colander from wuttafarm.web.views.farmos import FarmOSMasterView + +from wuttafarm.web.forms.schema import UsersType from wuttafarm.web.forms.widgets import AnimalImage @@ -123,16 +125,17 @@ class AnimalView(FarmOSMasterView): # add owners if owner := relationships.get("owner"): - owners = [] + data["owners"] = [] for owner_data in owner["data"]: - owners.append( - self.farmos_client.resource.get_id( - "user", "user", owner_data["id"] - ) + 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"], + } ) - data["owners"] = ", ".join( - [o["data"]["attributes"]["display_name"] for o in owners] - ) # add image urls if image := relationships.get("image"): @@ -184,6 +187,9 @@ class AnimalView(FarmOSMasterView): # is_castrated f.set_node("is_castrated", colander.Boolean()) + # owners + f.set_node("owners", UsersType(self.request)) + # notes f.set_widget("notes", "notes") diff --git a/src/wuttafarm/web/views/farmos/users.py b/src/wuttafarm/web/views/farmos/users.py new file mode 100644 index 0000000..317bfe3 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/users.py @@ -0,0 +1,139 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS Users +""" + +import datetime + +import colander + +from wuttaweb.forms.schema import WuttaDateTime + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class UserView(FarmOSMasterView): + """ + Master view for Farm Animals + """ + + model_name = "farmos_user" + model_title = "farmOS User" + model_title_plural = "farmOS Users" + + route_prefix = "farmos_users" + url_prefix = "/farmOS/users" + + farmos_refurl_path = "/people" + + grid_columns = [ + "display_name", + ] + + sort_defaults = "display_name" + + form_fields = [ + "display_name", + "name", + "mail", + "timezone", + "created", + "changed", + ] + + def get_grid_data(self, columns=None, session=None): + users = self.farmos_client.resource.get("user", "user") + return [self.normalize_user(u) for u in users["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # display_name + g.set_link("display_name") + g.set_searchable("display_name") + + def get_instance(self): + user = self.farmos_client.resource.get_id( + "user", "user", self.request.matchdict["uuid"] + ) + return self.normalize_user(user["data"]) + + def get_instance_title(self, user): + return user["display_name"] + + def normalize_user(self, user): + + if created := user["attributes"].get("created"): + created = datetime.datetime.fromisoformat(created) + created = self.app.localtime(created) + + if changed := user["attributes"].get("changed"): + changed = datetime.datetime.fromisoformat(changed) + changed = self.app.localtime(changed) + + return { + "uuid": user["id"], + "drupal_internal_id": user["attributes"].get("drupal_internal__uid"), + "display_name": user["attributes"]["display_name"], + "name": user["attributes"].get("name") or colander.null, + "mail": user["attributes"].get("mail") or colander.null, + "timezone": user["attributes"].get("timezone") or colander.null, + "created": created, + "changed": changed, + } + + def configure_form(self, form): + f = form + super().configure_form(f) + user = f.model_instance + + # created + f.set_node("created", WuttaDateTime()) + + # changed + f.set_node("changed", WuttaDateTime()) + + def get_xref_buttons(self, user): + if drupal_id := user["drupal_internal_id"]: + return [ + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url(f"/user/{drupal_id}"), + target="_blank", + icon_left="external-link-alt", + ), + ] + return None + + +def defaults(config, **kwargs): + base = globals() + + UserView = kwargs.get("UserView", base["UserView"]) + UserView.defaults(config) + + +def includeme(config): + defaults(config) From baacd1c15cd3aaa7362770f928f97c216595c99e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 14:53:06 -0600 Subject: [PATCH 02/14] feat: add view for farmOS animal types --- src/wuttafarm/web/forms/schema.py | 19 +++ src/wuttafarm/web/forms/widgets.py | 26 ++++ src/wuttafarm/web/menus.py | 5 + src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + .../web/views/farmos/animal_types.py | 136 ++++++++++++++++++ src/wuttafarm/web/views/farmos/animals.py | 14 +- 7 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 src/wuttafarm/web/views/farmos/animal_types.py diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py index 132c7e3..7a9878e 100644 --- a/src/wuttafarm/web/forms/schema.py +++ b/src/wuttafarm/web/forms/schema.py @@ -28,6 +28,25 @@ import json import colander +class AnimalTypeType(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 AnimalTypeWidget + + return AnimalTypeWidget(self.request, **kwargs) + + class UsersType(colander.SchemaType): def __init__(self, request, *args, **kwargs): diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py index 933e8a4..008c295 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -49,6 +49,32 @@ class AnimalImage(Widget): return super().serialize(field, cstruct, **kw) +class AnimalTypeWidget(Widget): + """ + Widget to display an "animal type" 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") + + animal_type = json.loads(cstruct) + return tags.link_to( + animal_type["name"], + self.request.route_url( + "farmos_animal_types.view", uuid=animal_type["uuid"] + ), + ) + + return super().serialize(field, cstruct, **kw) + + class UsersWidget(Widget): """ Widget to display the list of owners for an asset etc. diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 642f427..a715019 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -47,6 +47,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_animals", "perm": "farmos_animals.list", }, + { + "title": "Animal Types", + "route": "farmos_animal_types", + "perm": "farmos_animal_types.list", + }, {"type": "sep"}, { "title": "Users", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 278d669..26d5be3 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -48,6 +48,8 @@ class CommonView(base.CommonView): site_admin = session.query(model.Role).filter_by(name="Site Admin").first() if site_admin: site_admin_perms = [ + "farmos_animal_types.list", + "farmos_animal_types.view", "farmos_animals.list", "farmos_animals.view", "farmos_users.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index dd03b86..2f6e764 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -28,4 +28,5 @@ from .master import FarmOSMasterView def includeme(config): config.include("wuttafarm.web.views.farmos.users") + config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") diff --git a/src/wuttafarm/web/views/farmos/animal_types.py b/src/wuttafarm/web/views/farmos/animal_types.py new file mode 100644 index 0000000..a974242 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/animal_types.py @@ -0,0 +1,136 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS animal types +""" + +import datetime + +import colander + +from wuttaweb.forms.schema import WuttaDateTime + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class AnimalTypeView(FarmOSMasterView): + """ + Master view for Animal Types in farmOS. + """ + + model_name = "farmos_animal_type" + model_title = "farmOS Animal Type" + model_title_plural = "farmOS Animal Types" + + route_prefix = "farmos_animal_types" + url_prefix = "/farmOS/animal-types" + + farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview" + + grid_columns = [ + "name", + "description", + "changed", + ] + + sort_defaults = "name" + + form_fields = [ + "name", + "description", + "changed", + ] + + def get_grid_data(self, columns=None, session=None): + animal_types = self.farmos_client.resource.get("taxonomy_term", "animal_type") + return [self.normalize_animal_type(t) for t in animal_types["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + g.set_searchable("name") + + # changed + g.set_renderer("changed", "datetime") + + def get_instance(self): + animal_type = self.farmos_client.resource.get_id( + "taxonomy_term", "animal_type", self.request.matchdict["uuid"] + ) + return self.normalize_animal_type(animal_type["data"]) + + def get_instance_title(self, animal_type): + return animal_type["name"] + + def normalize_animal_type(self, animal_type): + + if changed := animal_type["attributes"]["changed"]: + changed = datetime.datetime.fromisoformat(changed) + changed = self.app.localtime(changed) + + if description := animal_type["attributes"]["description"]: + description = description["value"] + + return { + "uuid": animal_type["id"], + "drupal_internal_id": animal_type["attributes"]["drupal_internal__tid"], + "name": animal_type["attributes"]["name"], + "description": description or colander.null, + "changed": changed, + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # description + f.set_widget("description", "notes") + + # changed + f.set_node("changed", WuttaDateTime()) + + def get_xref_buttons(self, animal_type): + return [ + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url( + f"/taxonomy/term/{animal_type['drupal_internal_id']}" + ), + target="_blank", + icon_left="external-link-alt", + ), + ] + + +def defaults(config, **kwargs): + base = globals() + + AnimalTypeView = kwargs.get("AnimalTypeView", base["AnimalTypeView"]) + AnimalTypeView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py index b6228c7..96ae67d 100644 --- a/src/wuttafarm/web/views/farmos/animals.py +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -29,7 +29,7 @@ import colander from wuttafarm.web.views.farmos import FarmOSMasterView -from wuttafarm.web.forms.schema import UsersType +from wuttafarm.web.forms.schema import UsersType, AnimalTypeType from wuttafarm.web.forms.widgets import AnimalImage @@ -48,6 +48,7 @@ class AnimalView(FarmOSMasterView): farmos_refurl_path = "/assets/animal" labels = { + "animal_type": "Species / Breed", "is_castrated": "Castrated", "location_name": "Current Location", "raw_image_url": "Raw Image URL", @@ -67,7 +68,7 @@ class AnimalView(FarmOSMasterView): form_fields = [ "name", - "animal_type_name", + "animal_type", "birthdate", "sex", "is_castrated", @@ -113,7 +114,10 @@ class AnimalView(FarmOSMasterView): animal_type = self.farmos_client.resource.get_id( "taxonomy_term", "animal_type", animal_type["data"]["id"] ) - data["animal_type_name"] = animal_type["data"]["attributes"]["name"] + data["animal_type"] = { + "uuid": animal_type["data"]["id"], + "name": animal_type["data"]["attributes"]["name"], + } # add location if location := relationships.get("location"): @@ -170,7 +174,6 @@ class AnimalView(FarmOSMasterView): "uuid": animal["id"], "drupal_internal_id": animal["attributes"]["drupal_internal__id"], "name": animal["attributes"]["name"], - "species_breed": "", # TODO "birthdate": birthdate, "sex": animal["attributes"]["sex"], "is_castrated": animal["attributes"]["is_castrated"], @@ -184,6 +187,9 @@ class AnimalView(FarmOSMasterView): super().configure_form(f) animal = f.model_instance + # animal_type + f.set_node("animal_type", AnimalTypeType(self.request)) + # is_castrated f.set_node("is_castrated", colander.Boolean()) From d9ef55010043dbc2228f718b47cde8f3b90b51af Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 16:42:32 -0600 Subject: [PATCH 03/14] feat: add view for farmOS structures --- src/wuttafarm/web/forms/schema.py | 19 ++ src/wuttafarm/web/forms/widgets.py | 39 +++- src/wuttafarm/web/menus.py | 5 + src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/animals.py | 21 +- src/wuttafarm/web/views/farmos/master.py | 6 + src/wuttafarm/web/views/farmos/structures.py | 209 +++++++++++++++++++ 8 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 src/wuttafarm/web/views/farmos/structures.py diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py index 7a9878e..a38588a 100644 --- a/src/wuttafarm/web/forms/schema.py +++ b/src/wuttafarm/web/forms/schema.py @@ -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): diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py index 008c295..0ffb055 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -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. diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index a715019..dbeba44 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -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", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 26d5be3..a238983 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -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", ] diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index 2f6e764..52652b8 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -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") diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py index 96ae67d..8eca5af 100644 --- a/src/wuttafarm/web/views/farmos/animals.py +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -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): diff --git a/src/wuttafarm/web/views/farmos/master.py b/src/wuttafarm/web/views/farmos/master.py index eed04d1..59003d0 100644 --- a/src/wuttafarm/web/views/farmos/master.py +++ b/src/wuttafarm/web/views/farmos/master.py @@ -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() diff --git a/src/wuttafarm/web/views/farmos/structures.py b/src/wuttafarm/web/views/farmos/structures.py new file mode 100644 index 0000000..bbc4f1f --- /dev/null +++ b/src/wuttafarm/web/views/farmos/structures.py @@ -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 . +# +################################################################################ +""" +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) From 19b6738e5dfbe2e9ba3e1fd15604d76a969053a0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 17:03:35 -0600 Subject: [PATCH 04/14] feat: add view for farmOS asset types of limited value perhaps, but what the heck --- src/wuttafarm/web/menus.py | 5 + src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/asset_types.py | 101 ++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 src/wuttafarm/web/views/farmos/asset_types.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index dbeba44..0f46281 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -57,6 +57,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_structures", "perm": "farmos_structures.list", }, + { + "title": "Asset Types", + "route": "farmos_asset_types", + "perm": "farmos_asset_types.list", + }, {"type": "sep"}, { "title": "Users", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index a238983..0a4e72f 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -52,6 +52,8 @@ class CommonView(base.CommonView): "farmos_animal_types.view", "farmos_animals.list", "farmos_animals.view", + "farmos_asset_types.list", + "farmos_asset_types.view", "farmos_structures.list", "farmos_structures.view", "farmos_users.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index 52652b8..d7e699b 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -28,6 +28,7 @@ from .master import FarmOSMasterView def includeme(config): config.include("wuttafarm.web.views.farmos.users") + config.include("wuttafarm.web.views.farmos.asset_types") config.include("wuttafarm.web.views.farmos.structures") config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") diff --git a/src/wuttafarm/web/views/farmos/asset_types.py b/src/wuttafarm/web/views/farmos/asset_types.py new file mode 100644 index 0000000..75eebbe --- /dev/null +++ b/src/wuttafarm/web/views/farmos/asset_types.py @@ -0,0 +1,101 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS asset types +""" + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class AssetTypeView(FarmOSMasterView): + """ + View for farmOS asset types + """ + + model_name = "farmos_asset_type" + model_title = "farmOS Asset Type" + model_title_plural = "farmOS Asset Types" + + route_prefix = "farmos_asset_types" + url_prefix = "/farmOS/asset-types" + + grid_columns = [ + "label", + "description", + ] + + sort_defaults = "label" + + form_fields = [ + "label", + "description", + ] + + def get_grid_data(self, columns=None, session=None): + asset_types = self.farmos_client.resource.get("asset_type", "asset_type") + return [self.normalize_asset_type(t) for t in asset_types["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # label + g.set_link("label") + g.set_searchable("label") + + # description + g.set_searchable("description") + + def get_instance(self): + asset_type = self.farmos_client.resource.get_id( + "asset_type", "asset_type", self.request.matchdict["uuid"] + ) + return self.normalize_asset_type(asset_type["data"]) + + def get_instance_title(self, asset_type): + return asset_type["label"] + + def normalize_asset_type(self, asset_type): + return { + "uuid": asset_type["id"], + "drupal_internal_id": asset_type["attributes"]["drupal_internal__id"], + "label": asset_type["attributes"]["label"], + "description": asset_type["attributes"]["description"], + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # description + f.set_widget("description", "notes") + + +def defaults(config, **kwargs): + base = globals() + + AssetTypeView = kwargs.get("AssetTypeView", base["AssetTypeView"]) + AssetTypeView.defaults(config) + + +def includeme(config): + defaults(config) From ba926ec2de906af548f47b60a8cf31039f34be80 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 17:04:02 -0600 Subject: [PATCH 05/14] fix: ensure Buefy version matches what we use for custom css this is an alright solution for now, but may need to improve in the future once we look at Vue 3 etc. basically the only reason this solution isn't terrible, is because buefy 0.9.x (for Vue 2) is "stable" --- src/wuttafarm/web/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wuttafarm/web/app.py b/src/wuttafarm/web/app.py index 83c9817..5c59434 100644 --- a/src/wuttafarm/web/app.py +++ b/src/wuttafarm/web/app.py @@ -49,11 +49,16 @@ def main(global_config, **settings): app = wutta_config.get_app() path = app.resource_path("wuttafarm.web.static:css/wuttafarm-buefy.css") if os.path.exists(path): + # TODO: this is not robust enough, probably..but works for me/now wutta_config.setdefault( "wuttaweb.liburl.buefy_css", "/wuttafarm/css/wuttafarm-buefy.css" ) + # nb. ensure buefy version matches what we use for custom css + wutta_config.setdefault("wuttaweb.libver.buefy", "0.9.29") + wutta_config.setdefault("wuttaweb.libver.buefy_css", "0.9.29") + # bring in the rest of WuttaFarm pyramid_config.include("wuttafarm.web.static") pyramid_config.include("wuttafarm.web.subscribers") From 5005c3c9780523557ded3844aeafaa79c95373ef Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 17:21:42 -0600 Subject: [PATCH 06/14] feat: add view for farmOS groups --- src/wuttafarm/web/menus.py | 12 +- src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/groups.py | 164 +++++++++++++++++++++ 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 src/wuttafarm/web/views/farmos/groups.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 0f46281..60a3a41 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -48,15 +48,21 @@ class WuttaFarmMenuHandler(base.MenuHandler): "perm": "farmos_animals.list", }, { - "title": "Animal Types", - "route": "farmos_animal_types", - "perm": "farmos_animal_types.list", + "title": "Groups", + "route": "farmos_groups", + "perm": "farmos_groups.list", }, { "title": "Structures", "route": "farmos_structures", "perm": "farmos_structures.list", }, + {"type": "sep"}, + { + "title": "Animal Types", + "route": "farmos_animal_types", + "perm": "farmos_animal_types.list", + }, { "title": "Asset Types", "route": "farmos_asset_types", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 0a4e72f..23ed96b 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -54,6 +54,8 @@ class CommonView(base.CommonView): "farmos_animals.view", "farmos_asset_types.list", "farmos_asset_types.view", + "farmos_groups.list", + "farmos_groups.view", "farmos_structures.list", "farmos_structures.view", "farmos_users.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index d7e699b..93e1341 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -32,3 +32,4 @@ def includeme(config): config.include("wuttafarm.web.views.farmos.structures") config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") + config.include("wuttafarm.web.views.farmos.groups") diff --git a/src/wuttafarm/web/views/farmos/groups.py b/src/wuttafarm/web/views/farmos/groups.py new file mode 100644 index 0000000..4664a6b --- /dev/null +++ b/src/wuttafarm/web/views/farmos/groups.py @@ -0,0 +1,164 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS Groups +""" + +import datetime + +import colander + +from wuttafarm.web.views.farmos import FarmOSMasterView +from wuttaweb.forms.schema import WuttaDateTime +from wuttaweb.forms.widgets import WuttaDateTimeWidget + + +class GroupView(FarmOSMasterView): + """ + View for farmOS Groups + """ + + model_name = "farmos_group" + model_title = "farmOS Group" + model_title_plural = "farmOS Groups" + + route_prefix = "farmos_groups" + url_prefix = "/farmOS/groups" + + farmos_refurl_path = "/assets/group" + + grid_columns = [ + "name", + "is_fixed", + "is_location", + "status", + "changed", + ] + + sort_defaults = "name" + + form_fields = [ + "name", + "is_fixed", + "is_location", + "status", + "notes", + "created", + "changed", + ] + + def get_grid_data(self, columns=None, session=None): + groups = self.farmos_client.resource.get("asset", "group") + return [self.normalize_group(a) for a in groups["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + g.set_searchable("name") + + # is_fixed + g.set_renderer("is_fixed", "boolean") + + # is_location + g.set_renderer("is_location", "boolean") + + # changed + g.set_renderer("changed", "datetime") + + def get_instance(self): + + group = self.farmos_client.resource.get_id( + "asset", "group", self.request.matchdict["uuid"] + ) + + return self.normalize_group(group["data"]) + + def get_instance_title(self, group): + return group["name"] + + def normalize_group(self, group): + + if created := group["attributes"].get("created"): + created = datetime.datetime.fromisoformat(created) + created = self.app.localtime(created) + + if changed := group["attributes"].get("changed"): + changed = datetime.datetime.fromisoformat(changed) + changed = self.app.localtime(changed) + + return { + "uuid": group["id"], + "drupal_internal_id": group["attributes"]["drupal_internal__id"], + "name": group["attributes"]["name"], + "created": created, + "changed": changed, + "is_fixed": group["attributes"]["is_fixed"], + "is_location": group["attributes"]["is_location"], + "status": group["attributes"]["status"], + "notes": group["attributes"]["notes"]["value"], + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # 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)) + + def get_xref_buttons(self, group): + return [ + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url(f"/asset/{group['drupal_internal_id']}"), + target="_blank", + icon_left="external-link-alt", + ), + ] + + +def defaults(config, **kwargs): + base = globals() + + GroupView = kwargs.get("GroupView", base["GroupView"]) + GroupView.defaults(config) + + +def includeme(config): + defaults(config) From 233b2a2dab956062b3c412b74bfb1da2e5a29495 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 18:11:48 -0600 Subject: [PATCH 07/14] feat: add view for farmOS land assets --- src/wuttafarm/web/menus.py | 5 + src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/land_assets.py | 169 ++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 src/wuttafarm/web/views/farmos/land_assets.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 60a3a41..a9e66e9 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -57,6 +57,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_structures", "perm": "farmos_structures.list", }, + { + "title": "Land", + "route": "farmos_land_assets", + "perm": "farmos_land_assets.list", + }, {"type": "sep"}, { "title": "Animal Types", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 23ed96b..b13d43b 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -56,6 +56,8 @@ class CommonView(base.CommonView): "farmos_asset_types.view", "farmos_groups.list", "farmos_groups.view", + "farmos_land_assets.list", + "farmos_land_assets.view", "farmos_structures.list", "farmos_structures.view", "farmos_users.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index 93e1341..a194d64 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -29,6 +29,7 @@ from .master import FarmOSMasterView def includeme(config): config.include("wuttafarm.web.views.farmos.users") config.include("wuttafarm.web.views.farmos.asset_types") + config.include("wuttafarm.web.views.farmos.land_assets") config.include("wuttafarm.web.views.farmos.structures") config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") diff --git a/src/wuttafarm/web/views/farmos/land_assets.py b/src/wuttafarm/web/views/farmos/land_assets.py new file mode 100644 index 0000000..a496cc5 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/land_assets.py @@ -0,0 +1,169 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS Land Assets +""" + +import datetime + +import colander + +from wuttaweb.forms.schema import WuttaDateTime +from wuttaweb.forms.widgets import WuttaDateTimeWidget + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class LandAssetView(FarmOSMasterView): + """ + View for farmOS Land Assets + """ + + model_name = "farmos_land_asset" + model_title = "farmOS Land Asset" + model_title_plural = "farmOS Land Assets" + + route_prefix = "farmos_land_assets" + url_prefix = "/farmOS/land" + + farmos_refurl_path = "/assets/land" + + grid_columns = [ + "name", + "is_fixed", + "is_location", + "status", + "changed", + ] + + sort_defaults = "name" + + form_fields = [ + "name", + "is_fixed", + "is_location", + "status", + "notes", + "created", + "changed", + ] + + def get_grid_data(self, columns=None, session=None): + land_assets = self.farmos_client.resource.get("asset", "land") + return [self.normalize_land_asset(l) for l in land_assets["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + g.set_searchable("name") + + # is_fixed + g.set_renderer("is_fixed", "boolean") + + # is_location + g.set_renderer("is_location", "boolean") + + # created + g.set_renderer("created", "datetime") + + # changed + g.set_renderer("changed", "datetime") + + def get_instance(self): + land_asset = self.farmos_client.resource.get_id( + "asset", "land", self.request.matchdict["uuid"] + ) + return self.normalize_land_asset(land_asset["data"]) + + def get_instance_title(self, land_asset): + return land_asset["name"] + + def normalize_land_asset(self, land): + + if created := land["attributes"].get("created"): + created = datetime.datetime.fromisoformat(created) + created = self.app.localtime(created) + + if changed := land["attributes"].get("changed"): + changed = datetime.datetime.fromisoformat(changed) + changed = self.app.localtime(changed) + + if notes := land["attributes"]["notes"]: + notes = notes["value"] + + return { + "uuid": land["id"], + "drupal_internal_id": land["attributes"]["drupal_internal__id"], + "name": land["attributes"]["name"], + "created": created, + "changed": changed, + "is_fixed": land["attributes"]["is_fixed"], + "is_location": land["attributes"]["is_location"], + "status": land["attributes"]["status"], + "notes": notes or colander.null, + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # 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)) + + def get_xref_buttons(self, land): + return [ + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url(f"/asset/{land['drupal_internal_id']}"), + target="_blank", + icon_left="external-link-alt", + ), + ] + + +def defaults(config, **kwargs): + base = globals() + + LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"]) + LandAssetView.defaults(config) + + +def includeme(config): + defaults(config) From acba07aa0e601b563a1ad83c94906a27c2a0faeb Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 18:19:11 -0600 Subject: [PATCH 08/14] feat: add view for farmOS land types --- src/wuttafarm/web/menus.py | 5 ++ src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/land_types.py | 88 ++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 src/wuttafarm/web/views/farmos/land_types.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index a9e66e9..2897038 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -68,6 +68,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_animal_types", "perm": "farmos_animal_types.list", }, + { + "title": "Land Types", + "route": "farmos_land_types", + "perm": "farmos_land_types.list", + }, { "title": "Asset Types", "route": "farmos_asset_types", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index b13d43b..1c6e977 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -58,6 +58,8 @@ class CommonView(base.CommonView): "farmos_groups.view", "farmos_land_assets.list", "farmos_land_assets.view", + "farmos_land_types.list", + "farmos_land_types.view", "farmos_structures.list", "farmos_structures.view", "farmos_users.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index a194d64..e1dc122 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -29,6 +29,7 @@ from .master import FarmOSMasterView def includeme(config): config.include("wuttafarm.web.views.farmos.users") config.include("wuttafarm.web.views.farmos.asset_types") + config.include("wuttafarm.web.views.farmos.land_types") config.include("wuttafarm.web.views.farmos.land_assets") config.include("wuttafarm.web.views.farmos.structures") config.include("wuttafarm.web.views.farmos.animal_types") diff --git a/src/wuttafarm/web/views/farmos/land_types.py b/src/wuttafarm/web/views/farmos/land_types.py new file mode 100644 index 0000000..aadece8 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/land_types.py @@ -0,0 +1,88 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS land types +""" + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class LandTypeView(FarmOSMasterView): + """ + Master view for Land Types in farmOS. + """ + + model_name = "farmos_land_type" + model_title = "farmOS Land Type" + model_title_plural = "farmOS Land Types" + + route_prefix = "farmos_land_types" + url_prefix = "/farmOS/land-types" + + grid_columns = [ + "label", + ] + + sort_defaults = "label" + + form_fields = [ + "label", + ] + + def get_grid_data(self, columns=None, session=None): + land_types = self.farmos_client.resource.get("land_type", "land_type") + return [self.normalize_land_type(t) for t in land_types["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # label + g.set_link("label") + g.set_searchable("label") + + def get_instance(self): + land_type = self.farmos_client.resource.get_id( + "land_type", "land_type", self.request.matchdict["uuid"] + ) + return self.normalize_land_type(land_type["data"]) + + def get_instance_title(self, land_type): + return land_type["label"] + + def normalize_land_type(self, land_type): + return { + "uuid": land_type["id"], + "drupal_internal_id": land_type["attributes"]["drupal_internal__id"], + "label": land_type["attributes"]["label"], + } + + +def defaults(config, **kwargs): + base = globals() + + LandTypeView = kwargs.get("LandTypeView", base["LandTypeView"]) + LandTypeView.defaults(config) + + +def includeme(config): + defaults(config) From 7d65d3c5a2eb4f9004316909de4ad8dd58671798 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 18:22:40 -0600 Subject: [PATCH 09/14] feat: add view for farmOS structure types --- src/wuttafarm/web/menus.py | 5 ++ src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + .../web/views/farmos/structure_types.py | 90 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 src/wuttafarm/web/views/farmos/structure_types.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 2897038..618628e 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -68,6 +68,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_animal_types", "perm": "farmos_animal_types.list", }, + { + "title": "Structure Types", + "route": "farmos_structure_types", + "perm": "farmos_structure_types.list", + }, { "title": "Land Types", "route": "farmos_land_types", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 1c6e977..a84e232 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -60,6 +60,8 @@ class CommonView(base.CommonView): "farmos_land_assets.view", "farmos_land_types.list", "farmos_land_types.view", + "farmos_structure_types.list", + "farmos_structure_types.view", "farmos_structures.list", "farmos_structures.view", "farmos_users.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index e1dc122..4cc41ed 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -31,6 +31,7 @@ def includeme(config): config.include("wuttafarm.web.views.farmos.asset_types") config.include("wuttafarm.web.views.farmos.land_types") config.include("wuttafarm.web.views.farmos.land_assets") + config.include("wuttafarm.web.views.farmos.structure_types") config.include("wuttafarm.web.views.farmos.structures") config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") diff --git a/src/wuttafarm/web/views/farmos/structure_types.py b/src/wuttafarm/web/views/farmos/structure_types.py new file mode 100644 index 0000000..3fe4741 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/structure_types.py @@ -0,0 +1,90 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS structure types +""" + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class StructureTypeView(FarmOSMasterView): + """ + Master view for Structure Types in farmOS. + """ + + model_name = "farmos_structure_type" + model_title = "farmOS Structure Type" + model_title_plural = "farmOS Structure Types" + + route_prefix = "farmos_structure_types" + url_prefix = "/farmOS/structure-types" + + grid_columns = [ + "label", + ] + + sort_defaults = "label" + + form_fields = [ + "label", + ] + + def get_grid_data(self, columns=None, session=None): + structure_types = self.farmos_client.resource.get( + "structure_type", "structure_type" + ) + return [self.normalize_structure_type(t) for t in structure_types["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # label + g.set_link("label") + g.set_searchable("label") + + def get_instance(self): + structure_type = self.farmos_client.resource.get_id( + "structure_type", "structure_type", self.request.matchdict["uuid"] + ) + return self.normalize_structure_type(structure_type["data"]) + + def get_instance_title(self, structure_type): + return structure_type["label"] + + def normalize_structure_type(self, structure_type): + return { + "uuid": structure_type["id"], + "drupal_internal_id": structure_type["attributes"]["drupal_internal__id"], + "label": structure_type["attributes"]["label"], + } + + +def defaults(config, **kwargs): + base = globals() + + StructureTypeView = kwargs.get("StructureTypeView", base["StructureTypeView"]) + StructureTypeView.defaults(config) + + +def includeme(config): + defaults(config) From 33717bb0550308bef60c5d1396d4de5806eeb426 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 18:42:37 -0600 Subject: [PATCH 10/14] fix: add menu option, "Go to farmOS" --- src/wuttafarm/web/menus.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 618628e..d8796f8 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -38,10 +38,18 @@ class WuttaFarmMenuHandler(base.MenuHandler): ] def make_farmos_menu(self, request): + config = request.wutta_config + app = config.get_app() return { "title": "farmOS", "type": "menu", "items": [ + { + "title": "Go to farmOS", + "url": app.get_farmos_url(), + "target": "_blank", + }, + {"type": "sep"}, { "title": "Animals", "route": "farmos_animals", From f7d5d0ab1c70a94f11023f0a5853a1f2c23b7e7e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 19:25:13 -0600 Subject: [PATCH 11/14] feat: add view for farmOS log types --- src/wuttafarm/web/menus.py | 5 ++ src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + src/wuttafarm/web/views/farmos/log_types.py | 98 +++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 src/wuttafarm/web/views/farmos/log_types.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index d8796f8..d4e7715 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -91,6 +91,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_asset_types", "perm": "farmos_asset_types.list", }, + { + "title": "Log Types", + "route": "farmos_log_types", + "perm": "farmos_log_types.list", + }, {"type": "sep"}, { "title": "Users", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index a84e232..9f4100b 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -60,6 +60,8 @@ class CommonView(base.CommonView): "farmos_land_assets.view", "farmos_land_types.list", "farmos_land_types.view", + "farmos_log_types.list", + "farmos_log_types.view", "farmos_structure_types.list", "farmos_structure_types.view", "farmos_structures.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index 4cc41ed..cc389db 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -36,3 +36,4 @@ def includeme(config): config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") config.include("wuttafarm.web.views.farmos.groups") + config.include("wuttafarm.web.views.farmos.log_types") diff --git a/src/wuttafarm/web/views/farmos/log_types.py b/src/wuttafarm/web/views/farmos/log_types.py new file mode 100644 index 0000000..6e72f8f --- /dev/null +++ b/src/wuttafarm/web/views/farmos/log_types.py @@ -0,0 +1,98 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS log types +""" + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class LogTypeView(FarmOSMasterView): + """ + Master view for Log Types in farmOS. + """ + + model_name = "farmos_log_type" + model_title = "farmOS Log Type" + model_title_plural = "farmOS Log Types" + + route_prefix = "farmos_log_types" + url_prefix = "/farmOS/log-types" + + grid_columns = [ + "label", + "description", + ] + + sort_defaults = "label" + + form_fields = [ + "label", + "description", + ] + + def get_grid_data(self, columns=None, session=None): + log_types = self.farmos_client.resource.get("log_type", "log_type") + return [self.normalize_log_type(t) for t in log_types["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # label + g.set_link("label") + g.set_searchable("label") + + def get_instance(self): + log_type = self.farmos_client.resource.get_id( + "log_type", "log_type", self.request.matchdict["uuid"] + ) + return self.normalize_log_type(log_type["data"]) + + def get_instance_title(self, log_type): + return log_type["label"] + + def normalize_log_type(self, log_type): + return { + "uuid": log_type["id"], + "drupal_internal_id": log_type["attributes"]["drupal_internal__id"], + "label": log_type["attributes"]["label"], + "description": log_type["attributes"]["description"], + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # description + f.set_widget("description", "notes") + + +def defaults(config, **kwargs): + base = globals() + + LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"]) + LogTypeView.defaults(config) + + +def includeme(config): + defaults(config) From c778997239e65dd5a3d767ffe8de1df29b49e49a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 19:36:44 -0600 Subject: [PATCH 12/14] feat: add view for farmOS activity logs --- src/wuttafarm/web/menus.py | 6 + src/wuttafarm/web/views/common.py | 2 + src/wuttafarm/web/views/farmos/__init__.py | 1 + .../web/views/farmos/logs_activity.py | 136 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 src/wuttafarm/web/views/farmos/logs_activity.py diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index d4e7715..ab6f440 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -71,6 +71,12 @@ class WuttaFarmMenuHandler(base.MenuHandler): "perm": "farmos_land_assets.list", }, {"type": "sep"}, + { + "title": "Activity Logs", + "route": "farmos_logs_activity", + "perm": "farmos_logs_activity.list", + }, + {"type": "sep"}, { "title": "Animal Types", "route": "farmos_animal_types", diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 9f4100b..f46c018 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -62,6 +62,8 @@ class CommonView(base.CommonView): "farmos_land_types.view", "farmos_log_types.list", "farmos_log_types.view", + "farmos_logs_activity.list", + "farmos_logs_activity.view", "farmos_structure_types.list", "farmos_structure_types.view", "farmos_structures.list", diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index cc389db..deacd7d 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -37,3 +37,4 @@ def includeme(config): config.include("wuttafarm.web.views.farmos.animals") config.include("wuttafarm.web.views.farmos.groups") config.include("wuttafarm.web.views.farmos.log_types") + config.include("wuttafarm.web.views.farmos.logs_activity") diff --git a/src/wuttafarm/web/views/farmos/logs_activity.py b/src/wuttafarm/web/views/farmos/logs_activity.py new file mode 100644 index 0000000..61b4e85 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/logs_activity.py @@ -0,0 +1,136 @@ +# -*- 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 . +# +################################################################################ +""" +View for farmOS activity logs +""" + +import datetime + +import colander + +from wuttaweb.forms.schema import WuttaDateTime +from wuttaweb.forms.widgets import WuttaDateTimeWidget + +from wuttafarm.web.views.farmos import FarmOSMasterView + + +class ActivityLogView(FarmOSMasterView): + """ + View for farmOS activity logs + """ + + model_name = "farmos_activity_log" + model_title = "farmOS Activity Log" + model_title_plural = "farmOS Activity Logs" + + route_prefix = "farmos_logs_activity" + url_prefix = "/farmOS/logs/activity" + + farmos_refurl_path = "/logs/activity" + + grid_columns = [ + "name", + "timestamp", + "status", + ] + + sort_defaults = ("timestamp", "desc") + + form_fields = [ + "name", + "timestamp", + "status", + "notes", + ] + + def get_grid_data(self, columns=None, session=None): + logs = self.farmos_client.log.get("activity") + return [self.normalize_log(t) for t in logs["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + g.set_searchable("name") + + # timestamp + g.set_renderer("timestamp", "datetime") + + def get_instance(self): + log = self.farmos_client.log.get_id("activity", self.request.matchdict["uuid"]) + return self.normalize_log(log["data"]) + + def get_instance_title(self, log): + return log["name"] + + def normalize_log(self, log): + + if timestamp := log["attributes"]["timestamp"]: + timestamp = datetime.datetime.fromisoformat(timestamp) + timestamp = self.app.localtime(timestamp) + + if notes := log["attributes"]["notes"]: + notes = notes["value"] + + return { + "uuid": log["id"], + "drupal_internal_id": log["attributes"]["drupal_internal__id"], + "name": log["attributes"]["name"], + "timestamp": timestamp, + "status": log["attributes"]["status"], + "notes": notes or colander.null, + } + + def configure_form(self, form): + f = form + super().configure_form(f) + + # timestamp + f.set_node("timestamp", WuttaDateTime()) + f.set_widget("timestamp", WuttaDateTimeWidget(self.request)) + + # notes + f.set_widget("notes", "notes") + + def get_xref_buttons(self, log): + return [ + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url(f"/log/{log['drupal_internal_id']}"), + target="_blank", + icon_left="external-link-alt", + ), + ] + + +def defaults(config, **kwargs): + base = globals() + + ActivityLogView = kwargs.get("ActivityLogView", base["ActivityLogView"]) + ActivityLogView.defaults(config) + + +def includeme(config): + defaults(config) From 920811136ef5bccd6504a9f050682bf3bb5b8b92 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 8 Feb 2026 09:23:46 -0600 Subject: [PATCH 13/14] fix: add pyramid_exclog dependency mostly for convenience, since IMHO it is good to use in production --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index d2c6ef1..77bc3da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ license = {text = "GNU General Public License v3"} dependencies = [ "farmOS", "psycopg2", + "pyramid_exclog", "WuttaWeb[continuum]>=0.27.3", ] From ccb64c5c4d1eb0a228eecce295088287402b59fe Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 8 Feb 2026 09:24:28 -0600 Subject: [PATCH 14/14] =?UTF-8?q?bump:=20version=200.1.5=20=E2=86=92=200.2?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 21 +++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6142c..eabe9f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to WuttaFarm will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.2.0 (2026-02-08) + +### Feat + +- add view for farmOS activity logs +- add view for farmOS log types +- add view for farmOS structure types +- add view for farmOS land types +- add view for farmOS land assets +- add view for farmOS groups +- add view for farmOS asset types +- add view for farmOS structures +- add view for farmOS animal types +- add view for farmOS users + +### Fix + +- add pyramid_exclog dependency +- add menu option, "Go to farmOS" +- ensure Buefy version matches what we use for custom css + ## v0.1.5 (2026-02-07) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 77bc3da..48faefd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "WuttaFarm" -version = "0.1.5" +version = "0.2.0" description = "Web app to integrate with and extend farmOS" readme = "README.md" authors = [