Compare commits

...

6 commits

Author SHA1 Message Date
a5b699a52a bump: version 0.11.1 → 0.11.2 2026-03-21 20:21:52 -05:00
9707c36553 fix: use separate thread to sync changes to farmOS
i.e. when creating or editing an asset/log, or submitting quick eggs form
2026-03-21 20:18:32 -05:00
969497826d fix: avoid error if asset has no geometry 2026-03-21 15:24:36 -05:00
f0fa189bcd bump: version 0.11.0 → 0.11.1 2026-03-21 15:11:36 -05:00
cc4b94a7b8 fix: improve behavior when deleting mirrored record from farmOS
in some cases (maybe just dev?) the record does not exist in farmOS;
if so we should silently ignore.

and there seemed to be a problem with the sequence of events:

- user clicks delete in WF
- record is deleted from WF DB
- delete request sent to farmOS API
- webhook on farmOS side calls back to WF webhook URI

somewhere in there, in practice things seemed to hang after user
clicks delete.  i suppose the thread handling user's request is "tied
up" somehow, such that the webhook receiver can't process that
request?  that doesn't exactly make sense to me, but if we split off
to a separate thread to request the farmOS deletion, things seem to
work okay.  so maybe that idea is more accurate than i'd expect
2026-03-21 15:06:26 -05:00
ca5e1420e4 fix: use correct uuid when processing webhook to delete record 2026-03-21 15:03:06 -05:00
6 changed files with 109 additions and 25 deletions

View file

@ -5,6 +5,20 @@ 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.11.2 (2026-03-21)
### Fix
- use separate thread to sync changes to farmOS
- avoid error if asset has no geometry
## v0.11.1 (2026-03-21)
### Fix
- improve behavior when deleting mirrored record from farmOS
- use correct uuid when processing webhook to delete record
## v0.11.0 (2026-03-15) ## v0.11.0 (2026-03-15)
### Feat ### Feat

View file

@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "WuttaFarm" name = "WuttaFarm"
version = "0.11.0" version = "0.11.2"
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 = [
@ -34,7 +34,7 @@ dependencies = [
"pyramid_exclog", "pyramid_exclog",
"uvicorn[standard]", "uvicorn[standard]",
"WuttaSync", "WuttaSync",
"WuttaWeb[continuum]>=0.29.3", "WuttaWeb[continuum]>=0.30.1",
] ]

View file

@ -94,8 +94,7 @@ class ChangeProcessor:
return return
# delete corresponding record from our app # delete corresponding record from our app
obj = importer.get_target_object((change.uuid,)) if obj := importer.get_target_object((change.farmos_uuid,)):
if obj:
importer.delete_target_object(obj) importer.delete_target_object(obj)
# TODO: this should live elsewhere # TODO: this should live elsewhere

View file

@ -372,21 +372,23 @@ class AssetMasterView(WuttaFarmMasterView):
# TODO: eventually sync GIS data, avoid this API call? # TODO: eventually sync GIS data, avoid this API call?
client = get_farmos_client_for_user(self.request) client = get_farmos_client_for_user(self.request)
result = client.asset.get_id(asset.asset_type, asset.farmos_uuid) result = client.asset.get_id(asset.asset_type, asset.farmos_uuid)
geometry = result["data"]["attributes"]["intrinsic_geometry"] if geometry := result["data"]["attributes"]["intrinsic_geometry"]:
context["map_center"] = [geometry["lon"], geometry["lat"]] context["map_center"] = [geometry["lon"], geometry["lat"]]
context["map_bounds"] = [ context["map_bounds"] = [
[geometry["left"], geometry["bottom"]], [geometry["left"], geometry["bottom"]],
[geometry["right"], geometry["top"]], [geometry["right"], geometry["top"]],
] ]
if match := re.match( if match := re.match(
r"^POLYGON \(\((?P<points>[^\)]+)\)\)$", geometry["value"] r"^POLYGON \(\((?P<points>[^\)]+)\)\)$", geometry["value"]
): ):
points = match.group("points").split(", ") points = match.group("points").split(", ")
points = [[float(pt) for pt in pair.split(" ")] for pair in points] points = [
context["map_polygon"] = [points] [float(pt) for pt in pair.split(" ")] for pair in points
]
context["map_polygon"] = [points]
return context return context

View file

@ -23,6 +23,10 @@
Base class for WuttaFarm master views Base class for WuttaFarm master views
""" """
import threading
import time
import requests
from webhelpers2.html import tags from webhelpers2.html import tags
from wuttaweb.views import MasterView from wuttaweb.views import MasterView
@ -107,17 +111,36 @@ class WuttaFarmMasterView(MasterView):
f.set_readonly("drupal_id") f.set_readonly("drupal_id")
def persist(self, obj, session=None): def persist(self, obj, session=None):
session = session or self.Session()
# save per usual # save per usual
super().persist(obj, session) super().persist(obj, session)
# maybe also sync change to farmOS # maybe also sync change to farmOS
if self.app.is_farmos_mirror(): if self.app.is_farmos_mirror():
if self.creating:
session.flush() # need the new uuid
client = get_farmos_client_for_user(self.request) client = get_farmos_client_for_user(self.request)
self.auto_sync_to_farmos(client, obj) thread = threading.Thread(
target=self.auto_sync_to_farmos, args=(client, obj.uuid)
)
thread.start()
def auto_sync_to_farmos(self, client, obj): def auto_sync_to_farmos(self, client, uuid):
self.app.auto_sync_to_farmos(obj, client=client, require=False) model = self.app.model
model_class = self.get_model_class()
with self.app.short_session(commit=True) as session:
if user := session.query(model.User).filter_by(username="farmos").first():
session.info["continuum_user_id"] = user.uuid
obj = None
while not obj:
obj = session.get(model_class, uuid)
if not obj:
time.sleep(0.1)
self.app.auto_sync_to_farmos(obj, client=client, require=False)
def get_farmos_entity_type(self): def get_farmos_entity_type(self):
if self.farmos_entity_type: if self.farmos_entity_type:
@ -145,10 +168,24 @@ class WuttaFarmMasterView(MasterView):
# maybe delete from farmOS also # maybe delete from farmOS also
if farmos_uuid: if farmos_uuid:
entity_type = self.get_farmos_entity_type()
bundle = self.get_farmos_bundle()
client = get_farmos_client_for_user(self.request) client = get_farmos_client_for_user(self.request)
# nb. must use separate thread to avoid some kind of race
# condition (?) - seems as though maybe a "boomerang"
# effect is happening; this seems to help anyway
thread = threading.Thread(
target=self.delete_from_farmos, args=(client, farmos_uuid)
)
thread.start()
def delete_from_farmos(self, client, farmos_uuid):
entity_type = self.get_farmos_entity_type()
bundle = self.get_farmos_bundle()
try:
client.resource.delete(entity_type, bundle, farmos_uuid) client.resource.delete(entity_type, bundle, farmos_uuid)
except requests.HTTPError as exc:
# ignore if record not found in farmOS
if exc.response.status_code != 404:
raise
class TaxonomyMasterView(WuttaFarmMasterView): class TaxonomyMasterView(WuttaFarmMasterView):

View file

@ -24,6 +24,8 @@ Quick Form for "Eggs"
""" """
import json import json
import threading
import time
import colander import colander
from deform.widget import SelectWidget from deform.widget import SelectWidget
@ -331,13 +333,43 @@ class EggsQuickForm(QuickFormView):
session.flush() session.flush()
if self.app.is_farmos_mirror(): if self.app.is_farmos_mirror():
if new_unit: thread = threading.Thread(
self.app.auto_sync_to_farmos(unit, client=self.farmos_client) target=self.auto_sync_to_farmos,
self.app.auto_sync_to_farmos(quantity, client=self.farmos_client) args=(log.uuid, quantity.uuid, new_unit.uuid if new_unit else None),
self.app.auto_sync_to_farmos(log, client=self.farmos_client) )
thread.start()
return log return log
def auto_sync_to_farmos(self, log_uuid, quantity_uuid, new_unit_uuid):
model = self.app.model
with self.app.short_session(commit=True) as session:
if user := session.query(model.User).filter_by(username="farmos").first():
session.info["continuum_user_id"] = user.uuid
if new_unit_uuid:
new_unit = None
while not new_unit:
new_unit = session.get(model.Unit, new_unit_uuid)
if not new_unit:
time.sleep(0.1)
self.app.auto_sync_to_farmos(unit, client=self.farmos_client)
quantity = None
while not quantity:
quantity = session.get(model.StandardQuantity, quantity_uuid)
if not quantity:
time.sleep(0.1)
self.app.auto_sync_to_farmos(quantity, client=self.farmos_client)
log = None
while not log:
log = session.get(model.HarvestLog, log_uuid)
if not log:
time.sleep(0.1)
self.app.auto_sync_to_farmos(log, client=self.farmos_client)
def redirect_after_save(self, log): def redirect_after_save(self, log):
model = self.app.model model = self.app.model