From baacd1c15cd3aaa7362770f928f97c216595c99e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 14:53:06 -0600 Subject: [PATCH] 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())