Compare commits

...

14 commits

Author SHA1 Message Date
ccb64c5c4d bump: version 0.1.5 → 0.2.0 2026-02-08 09:24:28 -06:00
920811136e fix: add pyramid_exclog dependency
mostly for convenience, since IMHO it is good to use in production
2026-02-08 09:23:46 -06:00
c778997239 feat: add view for farmOS activity logs 2026-02-08 08:55:19 -06:00
f7d5d0ab1c feat: add view for farmOS log types 2026-02-07 19:25:13 -06:00
33717bb055 fix: add menu option, "Go to farmOS" 2026-02-07 18:48:56 -06:00
7d65d3c5a2 feat: add view for farmOS structure types 2026-02-07 18:48:56 -06:00
acba07aa0e feat: add view for farmOS land types 2026-02-07 18:48:56 -06:00
233b2a2dab feat: add view for farmOS land assets 2026-02-07 18:48:56 -06:00
5005c3c978 feat: add view for farmOS groups 2026-02-07 18:48:56 -06:00
ba926ec2de 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"
2026-02-07 18:48:56 -06:00
19b6738e5d feat: add view for farmOS asset types
of limited value perhaps, but what the heck
2026-02-07 18:48:56 -06:00
d9ef550100 feat: add view for farmOS structures 2026-02-07 18:48:54 -06:00
baacd1c15c feat: add view for farmOS animal types 2026-02-07 14:58:47 -06:00
7415504926 feat: add view for farmOS users 2026-02-07 14:26:49 -06:00
20 changed files with 1663 additions and 26 deletions

View file

@ -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/) 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). 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) ## v0.1.5 (2026-02-07)
### Fix ### Fix

View file

@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "WuttaFarm" name = "WuttaFarm"
version = "0.1.5" version = "0.2.0"
description = "Web app to integrate with and extend farmOS" description = "Web app to integrate with and extend farmOS"
readme = "README.md" readme = "README.md"
authors = [ authors = [
@ -31,6 +31,7 @@ license = {text = "GNU General Public License v3"}
dependencies = [ dependencies = [
"farmOS", "farmOS",
"psycopg2", "psycopg2",
"pyramid_exclog",
"WuttaWeb[continuum]>=0.27.3", "WuttaWeb[continuum]>=0.27.3",
] ]

View file

@ -49,11 +49,16 @@ def main(global_config, **settings):
app = wutta_config.get_app() app = wutta_config.get_app()
path = app.resource_path("wuttafarm.web.static:css/wuttafarm-buefy.css") path = app.resource_path("wuttafarm.web.static:css/wuttafarm-buefy.css")
if os.path.exists(path): if os.path.exists(path):
# TODO: this is not robust enough, probably..but works for me/now # TODO: this is not robust enough, probably..but works for me/now
wutta_config.setdefault( wutta_config.setdefault(
"wuttaweb.liburl.buefy_css", "/wuttafarm/css/wuttafarm-buefy.css" "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 # bring in the rest of WuttaFarm
pyramid_config.include("wuttafarm.web.static") pyramid_config.include("wuttafarm.web.static")
pyramid_config.include("wuttafarm.web.subscribers") pyramid_config.include("wuttafarm.web.subscribers")

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -23,18 +23,21 @@
Custom form widgets for WuttaFarm Custom form widgets for WuttaFarm
""" """
import json
import colander import colander
from deform.widget import Widget from deform.widget import Widget
from webhelpers2.html import HTML, tags from webhelpers2.html import HTML, tags
class AnimalImage(Widget): class ImageWidget(Widget):
"""
Widget to display an image URL for a record.
""" """
Widget to display an image URL for an animal.
TODO: this should be refactored to a more general name, once more def __init__(self, alt_text, *args, **kwargs):
types of images need to be supported. super().__init__(*args, **kwargs)
""" self.alt_text = alt_text
def serialize(self, field, cstruct, **kw): def serialize(self, field, cstruct, **kw):
readonly = kw.get("readonly", self.readonly) readonly = kw.get("readonly", self.readonly)
@ -42,6 +45,86 @@ class AnimalImage(Widget):
if cstruct in (colander.null, None): if cstruct in (colander.null, None):
return HTML.tag("span") return HTML.tag("span")
return tags.image(cstruct, "animal image", **kw) return tags.image(cstruct, self.alt_text, **kw)
return super().serialize(field, cstruct, **kw)
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) return super().serialize(field, cstruct, **kw)

View file

@ -38,14 +38,75 @@ class WuttaFarmMenuHandler(base.MenuHandler):
] ]
def make_farmos_menu(self, request): def make_farmos_menu(self, request):
config = request.wutta_config
app = config.get_app()
return { return {
"title": "farmOS", "title": "farmOS",
"type": "menu", "type": "menu",
"items": [ "items": [
{
"title": "Go to farmOS",
"url": app.get_farmos_url(),
"target": "_blank",
},
{"type": "sep"},
{ {
"title": "Animals", "title": "Animals",
"route": "farmos_animals", "route": "farmos_animals",
"perm": "farmos_animals.list", "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",
},
], ],
} }

View file

@ -48,8 +48,28 @@ class CommonView(base.CommonView):
site_admin = session.query(model.Role).filter_by(name="Site Admin").first() site_admin = session.query(model.Role).filter_by(name="Site Admin").first()
if site_admin: if site_admin:
site_admin_perms = [ site_admin_perms = [
"farmos_animal_types.list",
"farmos_animal_types.view",
"farmos_animals.list", "farmos_animals.list",
"farmos_animals.view", "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: for perm in site_admin_perms:
auth.grant_permission(site_admin, perm) auth.grant_permission(site_admin, perm)

View file

@ -27,4 +27,14 @@ from .master import FarmOSMasterView
def includeme(config): 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.animals")
config.include("wuttafarm.web.views.farmos.groups")
config.include("wuttafarm.web.views.farmos.log_types")
config.include("wuttafarm.web.views.farmos.logs_activity")

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -28,7 +28,9 @@ import datetime
import colander import colander
from wuttafarm.web.views.farmos import FarmOSMasterView 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): class AnimalView(FarmOSMasterView):
@ -46,11 +48,9 @@ class AnimalView(FarmOSMasterView):
farmos_refurl_path = "/assets/animal" farmos_refurl_path = "/assets/animal"
labels = { labels = {
"animal_type": "Species / Breed",
"is_castrated": "Castrated", "is_castrated": "Castrated",
"location_name": "Current Location", "location": "Current Location",
"raw_image_url": "Raw Image URL",
"large_image_url": "Large Image URL",
"thumbnail_image_url": "Thumbnail Image URL",
} }
grid_columns = [ grid_columns = [
@ -65,13 +65,13 @@ class AnimalView(FarmOSMasterView):
form_fields = [ form_fields = [
"name", "name",
"animal_type_name", "animal_type",
"birthdate", "birthdate",
"sex", "sex",
"is_castrated", "is_castrated",
"status", "status",
"owners", "owners",
"location_name", "location",
"notes", "notes",
"raw_image_url", "raw_image_url",
"large_image_url", "large_image_url",
@ -111,7 +111,10 @@ class AnimalView(FarmOSMasterView):
animal_type = self.farmos_client.resource.get_id( animal_type = self.farmos_client.resource.get_id(
"taxonomy_term", "animal_type", animal_type["data"]["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 # add location
if location := relationships.get("location"): if location := relationships.get("location"):
@ -119,19 +122,23 @@ class AnimalView(FarmOSMasterView):
location = self.farmos_client.resource.get_id( location = self.farmos_client.resource.get_id(
"asset", "structure", location["data"][0]["id"] "asset", "structure", location["data"][0]["id"]
) )
data["location_name"] = location["data"]["attributes"]["name"] data["location"] = {
"uuid": location["data"]["id"],
"name": location["data"]["attributes"]["name"],
}
# add owners # add owners
if owner := relationships.get("owner"): if owner := relationships.get("owner"):
owners = [] data["owners"] = []
for owner_data in owner["data"]: for owner_data in owner["data"]:
owners.append( owner = self.farmos_client.resource.get_id(
self.farmos_client.resource.get_id(
"user", "user", owner_data["id"] "user", "user", owner_data["id"]
) )
) data["owners"].append(
data["owners"] = ", ".join( {
[o["data"]["attributes"]["display_name"] for o in owners] "uuid": owner["data"]["id"],
"display_name": owner["data"]["attributes"]["display_name"],
}
) )
# add image urls # add image urls
@ -167,7 +174,6 @@ class AnimalView(FarmOSMasterView):
"uuid": animal["id"], "uuid": animal["id"],
"drupal_internal_id": animal["attributes"]["drupal_internal__id"], "drupal_internal_id": animal["attributes"]["drupal_internal__id"],
"name": animal["attributes"]["name"], "name": animal["attributes"]["name"],
"species_breed": "", # TODO
"birthdate": birthdate, "birthdate": birthdate,
"sex": animal["attributes"]["sex"], "sex": animal["attributes"]["sex"],
"is_castrated": animal["attributes"]["is_castrated"], "is_castrated": animal["attributes"]["is_castrated"],
@ -181,15 +187,24 @@ class AnimalView(FarmOSMasterView):
super().configure_form(f) super().configure_form(f)
animal = f.model_instance animal = f.model_instance
# animal_type
f.set_node("animal_type", AnimalTypeType(self.request))
# is_castrated # is_castrated
f.set_node("is_castrated", colander.Boolean()) f.set_node("is_castrated", colander.Boolean())
# location
f.set_node("location", StructureType(self.request))
# owners
f.set_node("owners", UsersType(self.request))
# notes # notes
f.set_widget("notes", "notes") f.set_widget("notes", "notes")
# image # image
if url := animal.get("large_image_url"): if url := animal.get("large_image_url"):
f.set_widget("image", AnimalImage()) f.set_widget("image", ImageWidget("animal image"))
f.set_default("image", url) f.set_default("image", url)
def get_xref_buttons(self, animal): def get_xref_buttons(self, animal):

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -45,6 +45,12 @@ class FarmOSMasterView(MasterView):
farmos_refurl_path = None farmos_refurl_path = None
labels = {
"raw_image_url": "Raw Image URL",
"large_image_url": "Large Image URL",
"thumbnail_image_url": "Thumbnail Image URL",
}
def __init__(self, request, context=None): def __init__(self, request, context=None):
super().__init__(request, context=context) super().__init__(request, context=context)
self.farmos_client = self.get_farmos_client() self.farmos_client = self.get_farmos_client()

View file

@ -0,0 +1,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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -0,0 +1,209 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttaFarm --Web app to integrate with and extend farmOS
# Copyright © 2026 Lance Edgar
#
# This file is part of WuttaFarm.
#
# WuttaFarm is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
View for farmOS Structures
"""
import datetime
import colander
from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.widgets import ImageWidget
class StructureView(FarmOSMasterView):
"""
View for farmOS Structures
"""
model_name = "farmos_structure"
model_title = "farmOS Structure"
model_title_plural = "farmOS Structures"
route_prefix = "farmos_structures"
url_prefix = "/farmOS/structures"
farmos_refurl_path = "/assets/structure"
grid_columns = [
"name",
"status",
"created",
"changed",
]
sort_defaults = "name"
form_fields = [
"name",
"status",
"structure_type",
"is_location",
"is_fixed",
"notes",
"created",
"changed",
"raw_image_url",
"large_image_url",
"thumbnail_image_url",
"image",
]
def get_grid_data(self, columns=None, session=None):
structures = self.farmos_client.resource.get("asset", "structure")
return [self.normalize_structure(s) for s in structures["data"]]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# name
g.set_link("name")
g.set_searchable("name")
# created
g.set_renderer("created", "datetime")
# changed
g.set_renderer("changed", "datetime")
def get_instance(self):
structure = self.farmos_client.resource.get_id(
"asset", "structure", self.request.matchdict["uuid"]
)
data = self.normalize_structure(structure["data"])
if relationships := structure["data"].get("relationships"):
# add owners
if owner := relationships.get("owner"):
data["owners"] = []
for owner_data in owner["data"]:
owner = self.farmos_client.resource.get_id(
"user", "user", owner_data["id"]
)
data["owners"].append(
{
"uuid": owner["data"]["id"],
"display_name": owner["data"]["attributes"]["display_name"],
}
)
# add image urls
if image := relationships.get("image"):
if image["data"]:
image = self.farmos_client.resource.get_id(
"file", "file", image["data"][0]["id"]
)
data["raw_image_url"] = self.app.get_farmos_url(
image["data"]["attributes"]["uri"]["url"]
)
# nb. other styles available: medium, wide
data["large_image_url"] = image["data"]["attributes"][
"image_style_uri"
]["large"]
data["thumbnail_image_url"] = image["data"]["attributes"][
"image_style_uri"
]["thumbnail"]
return data
def get_instance_title(self, structure):
return structure["name"]
def normalize_structure(self, structure):
if created := structure["attributes"].get("created"):
created = datetime.datetime.fromisoformat(created)
created = self.app.localtime(created)
if changed := structure["attributes"].get("changed"):
changed = datetime.datetime.fromisoformat(changed)
changed = self.app.localtime(changed)
return {
"uuid": structure["id"],
"drupal_internal_id": structure["attributes"]["drupal_internal__id"],
"name": structure["attributes"]["name"],
"structure_type": structure["attributes"]["structure_type"],
"is_fixed": structure["attributes"]["is_fixed"],
"is_location": structure["attributes"]["is_location"],
"notes": structure["attributes"]["notes"] or colander.null,
"status": structure["attributes"]["status"],
"created": created,
"changed": changed,
}
def configure_form(self, form):
f = form
super().configure_form(f)
structure = f.model_instance
# is_fixed
f.set_node("is_fixed", colander.Boolean())
# is_location
f.set_node("is_location", colander.Boolean())
# notes
f.set_widget("notes", "notes")
# created
f.set_node("created", WuttaDateTime())
f.set_widget("created", WuttaDateTimeWidget(self.request))
# changed
f.set_node("changed", WuttaDateTime())
f.set_widget("changed", WuttaDateTimeWidget(self.request))
# image
if url := structure.get("large_image_url"):
f.set_widget("image", ImageWidget("structure image"))
f.set_default("image", url)
def get_xref_buttons(self, structure):
drupal_id = structure["drupal_internal_id"]
return [
self.make_button(
"View in farmOS",
primary=True,
url=self.app.get_farmos_url(f"/asset/{drupal_id}"),
target="_blank",
icon_left="external-link-alt",
),
]
def defaults(config, **kwargs):
base = globals()
StructureView = kwargs.get("StructureView", base["StructureView"])
StructureView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)