From 74155049268d1bd454fe9689f5878548cdad6655 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Feb 2026 14:26:49 -0600 Subject: [PATCH] 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)