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 d2c6ef1..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 = [ @@ -31,6 +31,7 @@ license = {text = "GNU General Public License v3"} dependencies = [ "farmOS", "psycopg2", + "pyramid_exclog", "WuttaWeb[continuum]>=0.27.3", ] 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") diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py new file mode 100644 index 0000000..a38588a --- /dev/null +++ b/src/wuttafarm/web/forms/schema.py @@ -0,0 +1,85 @@ +# -*- 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 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 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): + 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..0ffb055 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -23,18 +23,21 @@ Custom form widgets for WuttaFarm """ +import json + import colander 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) @@ -42,6 +45,86 @@ 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) + + +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 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. + """ + + 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..ab6f440 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -38,14 +38,75 @@ 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", "perm": "farmos_animals.list", }, + { + "title": "Groups", + "route": "farmos_groups", + "perm": "farmos_groups.list", + }, + { + "title": "Structures", + "route": "farmos_structures", + "perm": "farmos_structures.list", + }, + { + "title": "Land", + "route": "farmos_land_assets", + "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", + "perm": "farmos_animal_types.list", + }, + { + "title": "Structure Types", + "route": "farmos_structure_types", + "perm": "farmos_structure_types.list", + }, + { + "title": "Land Types", + "route": "farmos_land_types", + "perm": "farmos_land_types.list", + }, + { + "title": "Asset Types", + "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", + "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..f46c018 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -48,8 +48,28 @@ 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_asset_types.list", + "farmos_asset_types.view", + "farmos_groups.list", + "farmos_groups.view", + "farmos_land_assets.list", + "farmos_land_assets.view", + "farmos_land_types.list", + "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", + "farmos_structures.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..deacd7d 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -27,4 +27,14 @@ 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.structure_types") + 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") + config.include("wuttafarm.web.views.farmos.log_types") + config.include("wuttafarm.web.views.farmos.logs_activity") 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 aa00412..8eca5af 100644 --- a/src/wuttafarm/web/views/farmos/animals.py +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -28,7 +28,9 @@ import datetime import colander from wuttafarm.web.views.farmos import FarmOSMasterView -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): @@ -46,11 +48,9 @@ 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", - "large_image_url": "Large Image URL", - "thumbnail_image_url": "Thumbnail Image URL", + "location": "Current Location", } grid_columns = [ @@ -65,13 +65,13 @@ class AnimalView(FarmOSMasterView): form_fields = [ "name", - "animal_type_name", + "animal_type", "birthdate", "sex", "is_castrated", "status", "owners", - "location_name", + "location", "notes", "raw_image_url", "large_image_url", @@ -111,7 +111,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"): @@ -119,20 +122,24 @@ 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"): - 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"): @@ -167,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"], @@ -181,15 +187,24 @@ 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()) + # location + f.set_node("location", StructureType(self.request)) + + # owners + f.set_node("owners", UsersType(self.request)) + # notes f.set_widget("notes", "notes") # 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/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) 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) 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) 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) 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) 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) 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/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) 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) 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)