diff --git a/CHANGELOG.md b/CHANGELOG.md index 579fc2a..794220d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,40 +5,6 @@ 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.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) - -### Feat - -- show basic map for "fixed" assets - -### Fix - -- include LogQuantity changes when viewing Log revision - -## v0.10.0 (2026-03-11) - -### Feat - -- add support for webhooks module in farmOS - -### Fix - -- remove print statement - ## v0.9.0 (2026-03-10) ### Feat diff --git a/pyproject.toml b/pyproject.toml index b702d8c..51737ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "WuttaFarm" -version = "0.11.2" +version = "0.9.0" description = "Web app to integrate with and extend farmOS" readme = "README.md" authors = [ @@ -34,7 +34,7 @@ dependencies = [ "pyramid_exclog", "uvicorn[standard]", "WuttaSync", - "WuttaWeb[continuum]>=0.30.1", + "WuttaWeb[continuum]>=0.29.2", ] diff --git a/src/wuttafarm/app.py b/src/wuttafarm/app.py index c5a581b..decd44f 100644 --- a/src/wuttafarm/app.py +++ b/src/wuttafarm/app.py @@ -270,7 +270,6 @@ class WuttaFarmAppHandler(base.AppHandler): then nothing will happen / import is silently skipped when there is no such importer. """ - model = self.app.model handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos") if model_name not in handler.importers: @@ -281,10 +280,6 @@ class WuttaFarmAppHandler(base.AppHandler): # nb. begin txn to establish the API client handler.begin_source_transaction(client) with self.short_session(commit=True) as session: - - if user := session.query(model.User).filter_by(username="farmos").first(): - session.info["continuum_user_id"] = user.uuid - handler.target_session = session importer = handler.get_importer(model_name, caches_target=False) normal = importer.normalize_source_object(obj) diff --git a/src/wuttafarm/cli/__init__.py b/src/wuttafarm/cli/__init__.py index 71d4994..cd06344 100644 --- a/src/wuttafarm/cli/__init__.py +++ b/src/wuttafarm/cli/__init__.py @@ -29,4 +29,3 @@ from .base import wuttafarm_typer from . import export_farmos from . import import_farmos from . import install -from . import process_webhooks diff --git a/src/wuttafarm/cli/process_webhooks.py b/src/wuttafarm/cli/process_webhooks.py deleted file mode 100644 index 9d66a70..0000000 --- a/src/wuttafarm/cli/process_webhooks.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- 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 . -# -################################################################################ -""" -WuttaFarm CLI -""" - -import logging -import time - -import typer -from oauthlib.oauth2 import BackendApplicationClient -from requests_oauthlib import OAuth2Session - -from wuttafarm.cli import wuttafarm_typer - - -log = logging.getLogger(__name__) - - -class ChangeProcessor: - - def __init__(self, config): - self.config = config - self.app = config.get_app() - - def process_change(self, change): - if change.deleted: - self.delete_record(change) - else: - self.import_record(change) - - def import_record(self, change): - token = self.get_farmos_oauth2_token() - client = self.app.get_farmos_client(token=token) - - full_type = f"{change.entity_type}--{change.bundle}" - record = client.resource.get_id( - change.entity_type, change.bundle, change.farmos_uuid - ) - - importer_map = self.get_importer_map() - model_name = importer_map[full_type] - - self.app.auto_sync_from_farmos(record["data"], model_name, client=client) - - def delete_record(self, change): - model = self.app.model - handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos") - - importer_map = self.get_importer_map() - full_type = f"{change.entity_type}--{change.bundle}" - model_name = importer_map[full_type] - - token = self.get_farmos_oauth2_token() - client = self.app.get_farmos_client(token=token) - - # nb. begin txn to establish the API client - handler.begin_source_transaction(client) - with self.app.short_session(commit=True) as session: - handler.target_session = session - importer = handler.get_importer(model_name, caches_target=False) - - # try to attribute change to 'farmos' user - if user := session.query(model.User).filter_by(username="farmos").first(): - session.info["continuum_user_id"] = user.uuid - - # only support importers with farmos_uuid as key - # (pretty sure that covers us..can revise if needed) - if importer.get_keys() != ["farmos_uuid"]: - log.warning( - "unsupported keys for %s importer: %s", - model_name, - importer.get_keys(), - ) - return - - # delete corresponding record from our app - if obj := importer.get_target_object((change.farmos_uuid,)): - importer.delete_target_object(obj) - - # TODO: this should live elsewhere - def get_farmos_oauth2_token(self): - - client_id = self.config.get( - "farmos.oauth2.importing.client_id", default="wuttafarm" - ) - client_secret = self.config.require("farmos.oauth2.importing.client_secret") - scope = self.config.get("farmos.oauth2.importing.scope", default="farm_manager") - - client = BackendApplicationClient(client_id=client_id) - oauth = OAuth2Session(client=client) - - return oauth.fetch_token( - token_url=self.app.get_farmos_url("/oauth/token"), - include_client_id=True, - client_secret=client_secret, - scope=scope, - ) - - # TODO: this should live elsewhere - def get_importer_map(self): - return { - "asset--animal": "AnimalAsset", - "asset--equipment": "EquipmentAsset", - "asset--group": "GroupAsset", - "asset--land": "LandAsset", - "asset--plant": "PlantAsset", - "asset--structure": "StructureAsset", - "asset--water": "WaterAsset", - "log--activity": "ActivityLog", - "log--harvest": "HarvestLog", - "log--medical": "MedicalLog", - "log--observation": "ObservationLog", - "log--seeding": "SeedingLog", - "quantity--material": "MaterialQuantity", - "quantity--standard": "StandardQuantity", - "taxonomy_term--animal_type": "AnimalType", - "taxonomy_term--equipment_type": "EquipmentType", - "taxonomy_term--plant_type": "PlantType", - "taxonomy_term--season": "Season", - "taxonomy_term--material_type": "MaterialType", - "taxonomy_term--unit": "Unit", - } - - -@wuttafarm_typer.command() -def process_webhooks( - ctx: typer.Context, -): - """ - Process incoming webhook requests from farmOS. - """ - config = ctx.parent.wutta_config - app = config.get_app() - model = app.model - processor = ChangeProcessor(config) - - while True: - with app.short_session(commit=True) as session: - - query = session.query(model.WebhookChange).order_by( - model.WebhookChange.received - ) - - # nb. fetch (at most) 2 changes instead of just 1; - # this will control time delay behavior below - if changes := query[:2]: - - # process first change - change = changes[0] - log.info("processing webhook change: %s", change) - processor.process_change(change) - session.delete(change) - - # minimal time delay if 2nd change exists - if len(changes) == 2: - time.sleep(0.1) - continue - - # nothing in queue, so wait 1 sec before checking again - time.sleep(1) diff --git a/src/wuttafarm/db/alembic/versions/dd4d4142b96d_add_webhookchange.py b/src/wuttafarm/db/alembic/versions/dd4d4142b96d_add_webhookchange.py deleted file mode 100644 index 5b566b2..0000000 --- a/src/wuttafarm/db/alembic/versions/dd4d4142b96d_add_webhookchange.py +++ /dev/null @@ -1,41 +0,0 @@ -"""add WebhookChange - -Revision ID: dd4d4142b96d -Revises: dca5b48a5562 -Create Date: 2026-03-10 22:31:54.324952 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "dd4d4142b96d" -down_revision: Union[str, None] = "dca5b48a5562" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # webhook_change - op.create_table( - "webhook_change", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("entity_type", sa.String(length=100), nullable=False), - sa.Column("bundle", sa.String(length=100), nullable=False), - sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("deleted", sa.Boolean(), nullable=False), - sa.Column("received", sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_webhook_change")), - ) - - -def downgrade() -> None: - - # webhook_change - op.drop_table("webhook_change") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index 716fd1c..d90272b 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -59,6 +59,3 @@ from .log_harvest import HarvestLog from .log_medical import MedicalLog from .log_observation import ObservationLog from .log_seeding import SeedingLog - -# misc. -from .webhook import WebhookChange diff --git a/src/wuttafarm/db/model/webhook.py b/src/wuttafarm/db/model/webhook.py deleted file mode 100644 index b96a572..0000000 --- a/src/wuttafarm/db/model/webhook.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- 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 . -# -################################################################################ -""" -Model definition for webhook changes -""" - -import sqlalchemy as sa - -from wuttjamaican.db import model -from wuttjamaican.util import make_utc - - -class WebhookChange(model.Base): - """ - Represents a "change" (create/update/delete) notification which - originated in farmOS and delivered via webhook. - - This table serves as a "FIFO queue" for processing the changes. - """ - - __tablename__ = "webhook_change" - - uuid = model.uuid_column() - - entity_type = sa.Column(sa.String(length=100), nullable=False) - bundle = sa.Column(sa.String(length=100), nullable=False) - farmos_uuid = sa.Column(model.UUID(), nullable=False) - - deleted = sa.Column(sa.Boolean(), nullable=False) - - received = sa.Column( - sa.DateTime(), - nullable=False, - default=make_utc, - doc=""" - Date and time when the change was obtained from the watcher thread. - """, - ) - - def __str__(self): - event_type = "delete" if self.deleted else "create/update" - return f"{event_type} {self.entity_type}--{self.bundle}: {self.farmos_uuid}" diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py index d60a96f..746f761 100644 --- a/src/wuttafarm/farmos/importing/wuttafarm.py +++ b/src/wuttafarm/farmos/importing/wuttafarm.py @@ -248,6 +248,8 @@ class EquipmentAssetImporter( def get_supported_fields(self): fields = list(super().get_supported_fields()) + + print(fields) fields.extend( [ "manufacturer", diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 4f5a47a..c739bad 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -339,8 +339,6 @@ class AnimalAssetImporter(AssetImporterBase): model_class = model.AnimalAsset - animal_types_by_farmos_uuid = None - def get_supported_fields(self): fields = list(super().get_supported_fields()) fields.extend( @@ -363,17 +361,6 @@ class AnimalAssetImporter(AssetImporterBase): if animal_type.farmos_uuid: self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type - def get_animal_type_by_farmos_uuid(self, uuid): - if self.animal_types_by_farmos_uuid is not None: - return self.animal_types_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.AnimalType) - .filter(model.AnimalType.farmos_uuid == uuid) - .first() - ) - def normalize_source_object(self, animal): """ """ animal_type_uuid = None @@ -381,7 +368,7 @@ class AnimalAssetImporter(AssetImporterBase): if animal_type := relationships.get("animal_type"): if animal_type["data"]: - if wf_animal_type := self.get_animal_type_by_farmos_uuid( + if wf_animal_type := self.animal_types_by_farmos_uuid.get( UUID(animal_type["data"]["id"]) ): animal_type_uuid = wf_animal_type.uuid @@ -513,8 +500,6 @@ class EquipmentAssetImporter(AssetImporterBase): model_class = model.EquipmentAsset - equipment_types_by_farmos_uuid = None - def get_supported_fields(self): fields = list(super().get_supported_fields()) fields.extend( @@ -535,17 +520,6 @@ class EquipmentAssetImporter(AssetImporterBase): equipment_type ) - def get_equipment_type_by_farmos_uuid(self, uuid): - if self.equipment_types_by_farmos_uuid is not None: - return self.equipment_types_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.EquipmentType) - .filter_by(farmos_uuid=uuid) - .first() - ) - def normalize_source_object(self, equipment): """ """ data = super().normalize_source_object(equipment) @@ -556,7 +530,7 @@ class EquipmentAssetImporter(AssetImporterBase): if equipment_type := relationships.get("equipment_type"): equipment_types = [] for equipment_type in equipment_type["data"]: - if wf_equipment_type := self.get_equipment_type_by_farmos_uuid( + if wf_equipment_type := self.equipment_types_by_farmos_uuid.get( UUID(equipment_type["id"]) ): equipment_types.append(wf_equipment_type.uuid) @@ -658,8 +632,6 @@ class LandAssetImporter(AssetImporterBase): model_class = model.LandAsset - land_types_by_id = None - def get_supported_fields(self): fields = list(super().get_supported_fields()) fields.extend( @@ -678,21 +650,10 @@ class LandAssetImporter(AssetImporterBase): for land_type in self.target_session.query(model.LandType): self.land_types_by_id[land_type.drupal_id] = land_type - def get_land_type_by_id(self, drupal_id): - if self.land_types_by_id is not None: - return self.land_types_by_id.get(drupal_id) - - model = self.app.model - return ( - self.target_session.query(model.LandType) - .filter_by(drupal_id=drupal_id) - .first() - ) - def normalize_source_object(self, land): """ """ land_type_id = land["attributes"]["land_type"] - land_type = self.get_land_type_by_id(land_type_id) + land_type = self.land_types_by_id.get(land_type_id) if not land_type: log.warning( "invalid land_type '%s' for farmOS Land Asset: %s", land_type_id, land @@ -797,9 +758,6 @@ class PlantAssetImporter(AssetImporterBase): model_class = model.PlantAsset - plant_types_by_farmos_uuid = None - seasons_by_farmos_uuid = None - def get_supported_fields(self): fields = list(super().get_supported_fields()) fields.extend( @@ -824,26 +782,6 @@ class PlantAssetImporter(AssetImporterBase): if season.farmos_uuid: self.seasons_by_farmos_uuid[season.farmos_uuid] = season - def get_plant_type_by_farmos_uuid(self, uuid): - if self.plant_types_by_farmos_uuid is not None: - return self.plant_types_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.PlantType) - .filter_by(farmos_uuid=uuid) - .first() - ) - - def get_season_by_farmos_uuid(self, uuid): - if self.seasons_by_farmos_uuid is not None: - return self.seasons_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.Season).filter_by(farmos_uuid=uuid).first() - ) - def normalize_source_object(self, plant): """ """ data = super().normalize_source_object(plant) @@ -855,7 +793,7 @@ class PlantAssetImporter(AssetImporterBase): if plant_type := relationships.get("plant_type"): plant_types = [] for plant_type in plant_type["data"]: - if wf_plant_type := self.get_plant_type_by_farmos_uuid( + if wf_plant_type := self.plant_types_by_farmos_uuid.get( UUID(plant_type["id"]) ): plant_types.append(wf_plant_type.uuid) @@ -865,7 +803,7 @@ class PlantAssetImporter(AssetImporterBase): if season := relationships.get("season"): seasons = [] for season in season["data"]: - if wf_season := self.get_season_by_farmos_uuid(UUID(season["id"])): + if wf_season := self.seasons_by_farmos_uuid.get(UUID(season["id"])): seasons.append(wf_season.uuid) else: log.warning("season not found: %s", season["id"]) @@ -948,8 +886,6 @@ class StructureAssetImporter(AssetImporterBase): model_class = model.StructureAsset - structure_types_by_id = None - def get_supported_fields(self): fields = list(super().get_supported_fields()) fields.extend( @@ -967,21 +903,10 @@ class StructureAssetImporter(AssetImporterBase): for structure_type in self.target_session.query(model.StructureType): self.structure_types_by_id[structure_type.drupal_id] = structure_type - def get_structure_type_by_id(self, drupal_id): - if self.structure_types_by_id is not None: - return self.structure_types_by_id.get(drupal_id) - - model = self.app.model - return ( - self.target_session.query(model.StructureType) - .filter_by(drupal_id=drupal_id) - .first() - ) - def normalize_source_object(self, structure): """ """ structure_type_id = structure["attributes"]["structure_type"] - structure_type = self.get_structure_type_by_id(structure_type_id) + structure_type = self.structure_types_by_id.get(structure_type_id) if not structure_type: log.warning( "invalid structure_type '%s' for farmOS Structure Asset: %s", diff --git a/src/wuttafarm/web/templates/appinfo/configure.mako b/src/wuttafarm/web/templates/appinfo/configure.mako index 26d6a54..912eef0 100644 --- a/src/wuttafarm/web/templates/appinfo/configure.mako +++ b/src/wuttafarm/web/templates/appinfo/configure.mako @@ -57,11 +57,6 @@ - - - - % endif -
- - ## main form -
- ${parent.page_content()} -
- - ## location map - % if map_polygon: -
- % endif - -
- - - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - % if map_polygon: - - % endif + ${parent.page_content()} diff --git a/src/wuttafarm/web/templates/base.mako b/src/wuttafarm/web/templates/base.mako index caa5c67..b28b52f 100644 --- a/src/wuttafarm/web/templates/base.mako +++ b/src/wuttafarm/web/templates/base.mako @@ -1,16 +1,6 @@ <%inherit file="wuttaweb:templates/base.mako" /> <%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" /> -<%def name="head_tags()"> - ${parent.head_tags()} - - ## TODO: this likely does not belong in the base template, and should be - ## included per template where actually needed. but this is easier for now. - - - - - <%def name="index_title_controls()"> ${parent.index_title_controls()} diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index b663cf5..e66f479 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -73,7 +73,3 @@ def includeme(config): # views for farmOS if mode != enum.FARMOS_INTEGRATION_MODE_NONE: config.include("wuttafarm.web.views.farmos") - - # webhook views (only for mirror mode) - if mode == enum.FARMOS_INTEGRATION_MODE_MIRROR: - config.include("wuttafarm.web.views.webhooks") diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py index 1ada778..64f4dbc 100644 --- a/src/wuttafarm/web/views/assets.py +++ b/src/wuttafarm/web/views/assets.py @@ -23,7 +23,6 @@ Master view for Assets """ -import re from collections import OrderedDict from webhelpers2.html import tags @@ -360,38 +359,6 @@ class AssetMasterView(WuttaFarmMasterView): return buttons - def get_template_context(self, context): - context = super().get_template_context(context) - - if self.viewing: - asset = context["instance"] - - # add location geometry if applicable - if asset.is_fixed and asset.farmos_uuid and not self.app.is_standalone(): - - # TODO: eventually sync GIS data, avoid this API call? - client = get_farmos_client_for_user(self.request) - result = client.asset.get_id(asset.asset_type, asset.farmos_uuid) - if geometry := result["data"]["attributes"]["intrinsic_geometry"]: - - context["map_center"] = [geometry["lon"], geometry["lat"]] - - context["map_bounds"] = [ - [geometry["left"], geometry["bottom"]], - [geometry["right"], geometry["top"]], - ] - - if match := re.match( - r"^POLYGON \(\((?P[^\)]+)\)\)$", geometry["value"] - ): - points = match.group("points").split(", ") - points = [ - [float(pt) for pt in pair.split(" ")] for pair in points - ] - context["map_polygon"] = [points] - - return context - def get_version_joins(self): """ We override this to declare the relationship between the diff --git a/src/wuttafarm/web/views/logs.py b/src/wuttafarm/web/views/logs.py index 2a4e6e0..3d91ba1 100644 --- a/src/wuttafarm/web/views/logs.py +++ b/src/wuttafarm/web/views/logs.py @@ -491,7 +491,6 @@ class LogMasterView(WuttaFarmMasterView): return super().get_version_joins() + [ model.Log, (model.LogAsset, "log_uuid", "uuid"), - (model.LogQuantity, "log_uuid", "uuid"), ] diff --git a/src/wuttafarm/web/views/master.py b/src/wuttafarm/web/views/master.py index c828b96..d9fe986 100644 --- a/src/wuttafarm/web/views/master.py +++ b/src/wuttafarm/web/views/master.py @@ -23,10 +23,6 @@ Base class for WuttaFarm master views """ -import threading -import time - -import requests from webhelpers2.html import tags from wuttaweb.views import MasterView @@ -111,36 +107,17 @@ class WuttaFarmMasterView(MasterView): f.set_readonly("drupal_id") def persist(self, obj, session=None): - session = session or self.Session() # save per usual super().persist(obj, session) # maybe also sync change to farmOS if self.app.is_farmos_mirror(): - if self.creating: - session.flush() # need the new uuid client = get_farmos_client_for_user(self.request) - thread = threading.Thread( - target=self.auto_sync_to_farmos, args=(client, obj.uuid) - ) - thread.start() + self.auto_sync_to_farmos(client, obj) - def auto_sync_to_farmos(self, client, uuid): - 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 auto_sync_to_farmos(self, client, obj): + self.app.auto_sync_to_farmos(obj, client=client, require=False) def get_farmos_entity_type(self): if self.farmos_entity_type: @@ -168,24 +145,10 @@ class WuttaFarmMasterView(MasterView): # maybe delete from farmOS also if farmos_uuid: + entity_type = self.get_farmos_entity_type() + bundle = self.get_farmos_bundle() 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) - except requests.HTTPError as exc: - # ignore if record not found in farmOS - if exc.response.status_code != 404: - raise class TaxonomyMasterView(WuttaFarmMasterView): diff --git a/src/wuttafarm/web/views/quick/eggs.py b/src/wuttafarm/web/views/quick/eggs.py index fded73c..8aae46e 100644 --- a/src/wuttafarm/web/views/quick/eggs.py +++ b/src/wuttafarm/web/views/quick/eggs.py @@ -24,8 +24,6 @@ Quick Form for "Eggs" """ import json -import threading -import time import colander from deform.widget import SelectWidget @@ -333,43 +331,13 @@ class EggsQuickForm(QuickFormView): session.flush() if self.app.is_farmos_mirror(): - thread = threading.Thread( - target=self.auto_sync_to_farmos, - args=(log.uuid, quantity.uuid, new_unit.uuid if new_unit else None), - ) - thread.start() + if new_unit: + self.app.auto_sync_to_farmos(unit, client=self.farmos_client) + self.app.auto_sync_to_farmos(quantity, client=self.farmos_client) + self.app.auto_sync_to_farmos(log, client=self.farmos_client) 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): model = self.app.model diff --git a/src/wuttafarm/web/views/webhooks.py b/src/wuttafarm/web/views/webhooks.py deleted file mode 100644 index f5a5db5..0000000 --- a/src/wuttafarm/web/views/webhooks.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- 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 . -# -################################################################################ -""" -Views for use as webhooks -""" - -import logging -from uuid import UUID - -from wuttaweb.views import View -from wuttaweb.db import Session - - -log = logging.getLogger(__name__) - - -class WebhookView(View): - """ - Webhook views - """ - - def farmos_webhook(self): - model = self.app.model - session = Session() - - try: - data = self.request.json - log.debug("got webhook payload: %s", data) - - _, entity_type, event_type = data["event"].split(":") - - uuid = data["entity"]["uuid"][0]["value"] - if entity_type == "taxonomy_term": - bundle = data["entity"]["vid"][0]["target_id"] - else: - bundle = data["entity"]["type"][0]["target_id"] - - change = model.WebhookChange( - entity_type=entity_type, - bundle=bundle, - farmos_uuid=UUID(uuid), - deleted=event_type == "delete", - ) - session.add(change) - - except: - log.exception("failed to process webhook request") - - return {} - - @classmethod - def defaults(cls, config): - cls._webhook_defaults(config) - - @classmethod - def _webhook_defaults(cls, config): - - # farmos webhook - config.add_route("webhooks.farmos", "/farmos/webhook", request_method="POST") - config.add_view( - cls, - attr="farmos_webhook", - route_name="webhooks.farmos", - require_csrf=False, - renderer="json", - ) - - -def defaults(config, **kwargs): - base = globals() - - WebhookView = kwargs.get("WebhookView", base["WebhookView"]) - WebhookView.defaults(config) - - -def includeme(config): - defaults(config)