feat: add basic support for WuttaFarm → farmOS export
typical CLI export tool, but also the export happens automatically when create or edit of record happens in wuttafarm supported models: - AnimalType - AnimalAsset - GroupAsset - LandAsset - StructureAsset
This commit is contained in:
parent
6677fe1e23
commit
da9b559752
14 changed files with 765 additions and 9 deletions
|
|
@ -58,6 +58,7 @@ wuttafarm = "wuttafarm.app:WuttaFarmAppProvider"
|
||||||
"wuttafarm" = "wuttafarm.web.menus:WuttaFarmMenuHandler"
|
"wuttafarm" = "wuttafarm.web.menus:WuttaFarmMenuHandler"
|
||||||
|
|
||||||
[project.entry-points."wuttasync.importing"]
|
[project.entry-points."wuttasync.importing"]
|
||||||
|
"export.to_farmos.from_wuttafarm" = "wuttafarm.farmos.importing.wuttafarm:FromWuttaFarmToFarmOS"
|
||||||
"import.to_wuttafarm.from_farmos" = "wuttafarm.importing.farmos:FromFarmOSToWuttaFarm"
|
"import.to_wuttafarm.from_farmos" = "wuttafarm.importing.farmos:FromFarmOSToWuttaFarm"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,38 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
handler = self.get_farmos_handler()
|
handler = self.get_farmos_handler()
|
||||||
return handler.is_farmos_4x(*args, **kwargs)
|
return handler.is_farmos_4x(*args, **kwargs)
|
||||||
|
|
||||||
|
def export_to_farmos(self, obj, require=True):
|
||||||
|
"""
|
||||||
|
Export the given object to farmOS, using configured handler.
|
||||||
|
|
||||||
|
This should ensure the given object is also *updated* with the
|
||||||
|
farmOS UUID and Drupal ID, when new record is created in
|
||||||
|
farmOS.
|
||||||
|
|
||||||
|
:param obj: Any data object in WuttaFarm, e.g. AnimalAsset
|
||||||
|
instance.
|
||||||
|
|
||||||
|
:param require: If true, this will *require* the export
|
||||||
|
handler to support objects of the given type. If false,
|
||||||
|
then nothing will happen / export is silently skipped when
|
||||||
|
there is no such exporter.
|
||||||
|
"""
|
||||||
|
handler = self.app.get_import_handler("export.to_farmos.from_wuttafarm")
|
||||||
|
|
||||||
|
model_name = type(obj).__name__
|
||||||
|
if model_name not in handler.importers:
|
||||||
|
if require:
|
||||||
|
raise ValueError(f"no exporter found for {model_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# nb. begin txn to establish the API client
|
||||||
|
# TODO: should probably use current user oauth2 token instead
|
||||||
|
# of always making a new one here, which is what happens IIUC
|
||||||
|
handler.begin_target_transaction()
|
||||||
|
importer = handler.get_importer(model_name, caches_target=False)
|
||||||
|
normal = importer.normalize_source_object(obj)
|
||||||
|
importer.process_data(source_data=[normal])
|
||||||
|
|
||||||
|
|
||||||
class WuttaFarmAppProvider(base.AppProvider):
|
class WuttaFarmAppProvider(base.AppProvider):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,6 @@ WuttaFarm CLI
|
||||||
from .base import wuttafarm_typer
|
from .base import wuttafarm_typer
|
||||||
|
|
||||||
# nb. must bring in all modules for discovery to work
|
# nb. must bring in all modules for discovery to work
|
||||||
|
from . import export_farmos
|
||||||
from . import import_farmos
|
from . import import_farmos
|
||||||
from . import install
|
from . import install
|
||||||
|
|
|
||||||
41
src/wuttafarm/cli/export_farmos.py
Normal file
41
src/wuttafarm/cli/export_farmos.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
See also: :ref:`wuttafarm-export-farmos`
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from wuttasync.cli import import_command, ImportCommandHandler
|
||||||
|
|
||||||
|
from wuttafarm.cli import wuttafarm_typer
|
||||||
|
|
||||||
|
|
||||||
|
@wuttafarm_typer.command()
|
||||||
|
@import_command
|
||||||
|
def export_farmos(ctx: typer.Context, **kwargs):
|
||||||
|
"""
|
||||||
|
Export data from WuttaFarm to farmOS API
|
||||||
|
"""
|
||||||
|
config = ctx.parent.wutta_config
|
||||||
|
handler = ImportCommandHandler(config, key="export.to_farmos.from_wuttafarm")
|
||||||
|
handler.run(ctx)
|
||||||
|
|
@ -26,6 +26,12 @@ Email sending config for WuttaFarm
|
||||||
from wuttasync.emails import ImportExportWarning
|
from wuttasync.emails import ImportExportWarning
|
||||||
|
|
||||||
|
|
||||||
|
class export_to_farmos_from_wuttafarm_warning(ImportExportWarning):
|
||||||
|
"""
|
||||||
|
Diff warning for WuttaFarm → farmOS export.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class import_to_wuttafarm_from_farmos_warning(ImportExportWarning):
|
class import_to_wuttafarm_from_farmos_warning(ImportExportWarning):
|
||||||
"""
|
"""
|
||||||
Diff warning for farmOS → WuttaFarm import.
|
Diff warning for farmOS → WuttaFarm import.
|
||||||
|
|
|
||||||
26
src/wuttafarm/farmos/importing/__init__.py
Normal file
26
src/wuttafarm/farmos/importing/__init__.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Importing data *into* farmOS
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import model
|
||||||
365
src/wuttafarm/farmos/importing/model.py
Normal file
365
src/wuttafarm/farmos/importing/model.py
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Importer models targeting farmOS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from wuttasync.importing import Importer
|
||||||
|
|
||||||
|
|
||||||
|
class ToFarmOS(Importer):
|
||||||
|
"""
|
||||||
|
Base class for data importer targeting the farmOS API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "uuid"
|
||||||
|
caches_target = True
|
||||||
|
|
||||||
|
def format_datetime(self, dt):
|
||||||
|
"""
|
||||||
|
Convert a WuttaFarm datetime object to the format required for
|
||||||
|
pushing to the farmOS API.
|
||||||
|
"""
|
||||||
|
if dt is None:
|
||||||
|
return None
|
||||||
|
dt = self.app.localtime(dt)
|
||||||
|
return dt.timestamp()
|
||||||
|
|
||||||
|
def normalize_datetime(self, dt):
|
||||||
|
"""
|
||||||
|
Convert a farmOS datetime value to naive UTC used by
|
||||||
|
WuttaFarm.
|
||||||
|
|
||||||
|
:param dt: Date/time string value "as-is" from the farmOS API.
|
||||||
|
|
||||||
|
:returns: Equivalent naive UTC ``datetime``
|
||||||
|
"""
|
||||||
|
if dt is None:
|
||||||
|
return None
|
||||||
|
dt = datetime.datetime.fromisoformat(dt)
|
||||||
|
return self.app.make_utc(dt)
|
||||||
|
|
||||||
|
|
||||||
|
class ToFarmOSAsset(ToFarmOS):
|
||||||
|
"""
|
||||||
|
Base class for asset data importer targeting the farmOS API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
farmos_asset_type = None
|
||||||
|
|
||||||
|
def get_target_objects(self, **kwargs):
|
||||||
|
assets = self.farmos_client.asset.get(self.farmos_asset_type)
|
||||||
|
return assets["data"]
|
||||||
|
|
||||||
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
# fetch from cache, if applicable
|
||||||
|
if self.caches_target:
|
||||||
|
return super().get_target_object(key)
|
||||||
|
|
||||||
|
# okay now must fetch via API
|
||||||
|
if self.get_keys() != ["uuid"]:
|
||||||
|
raise ValueError("must use uuid key for this to work")
|
||||||
|
uuid = key[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
asset = self.farmos_client.asset.get_id(self.farmos_asset_type, str(uuid))
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
return asset["data"]
|
||||||
|
|
||||||
|
def create_target_object(self, key, source_data):
|
||||||
|
if source_data.get("__ignoreme__"):
|
||||||
|
return None
|
||||||
|
if self.dry_run:
|
||||||
|
return source_data
|
||||||
|
|
||||||
|
payload = self.get_asset_payload(source_data)
|
||||||
|
result = self.farmos_client.asset.send(self.farmos_asset_type, payload)
|
||||||
|
normal = self.normalize_target_object(result["data"])
|
||||||
|
normal["_new_object"] = result["data"]
|
||||||
|
return normal
|
||||||
|
|
||||||
|
def update_target_object(self, asset, source_data, target_data=None):
|
||||||
|
if self.dry_run:
|
||||||
|
return asset
|
||||||
|
|
||||||
|
payload = self.get_asset_payload(source_data)
|
||||||
|
payload["id"] = str(source_data["uuid"])
|
||||||
|
result = self.farmos_client.asset.send(self.farmos_asset_type, payload)
|
||||||
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
|
def normalize_target_object(self, asset):
|
||||||
|
|
||||||
|
if notes := asset["attributes"]["notes"]:
|
||||||
|
notes = notes["value"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uuid": UUID(asset["id"]),
|
||||||
|
"asset_name": asset["attributes"]["name"],
|
||||||
|
"is_location": asset["attributes"]["is_location"],
|
||||||
|
"is_fixed": asset["attributes"]["is_fixed"],
|
||||||
|
"notes": notes,
|
||||||
|
"archived": asset["attributes"]["archived"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_asset_payload(self, source_data):
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if "asset_name" in self.fields:
|
||||||
|
attrs["name"] = source_data["asset_name"]
|
||||||
|
if "is_location" in self.fields:
|
||||||
|
attrs["is_location"] = source_data["is_location"]
|
||||||
|
if "is_fixed" in self.fields:
|
||||||
|
attrs["is_fixed"] = source_data["is_fixed"]
|
||||||
|
if "notes" in self.fields:
|
||||||
|
attrs["notes"] = {"value": source_data["notes"]}
|
||||||
|
if "archived" in self.fields:
|
||||||
|
attrs["archived"] = source_data["archived"]
|
||||||
|
|
||||||
|
payload = {"attributes": attrs}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class AnimalAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
|
model_title = "AnimalAsset"
|
||||||
|
farmos_asset_type = "animal"
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"animal_type_uuid",
|
||||||
|
"sex",
|
||||||
|
"is_sterile",
|
||||||
|
"birthdate",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_target_object(self, animal):
|
||||||
|
data = super().normalize_target_object(animal)
|
||||||
|
data.update(
|
||||||
|
{
|
||||||
|
"animal_type_uuid": UUID(
|
||||||
|
animal["relationships"]["animal_type"]["data"]["id"]
|
||||||
|
),
|
||||||
|
"sex": animal["attributes"]["sex"],
|
||||||
|
"is_sterile": animal["attributes"]["is_sterile"],
|
||||||
|
"birthdate": self.normalize_datetime(animal["attributes"]["birthdate"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_asset_payload(self, source_data):
|
||||||
|
payload = super().get_asset_payload(source_data)
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if "sex" in self.fields:
|
||||||
|
attrs["sex"] = source_data["sex"]
|
||||||
|
if "is_sterile" in self.fields:
|
||||||
|
attrs["is_sterile"] = source_data["is_sterile"]
|
||||||
|
if "birthdate" in self.fields:
|
||||||
|
attrs["birthdate"] = self.format_datetime(source_data["birthdate"])
|
||||||
|
|
||||||
|
rels = {}
|
||||||
|
if "animal_type_uuid" in self.fields:
|
||||||
|
rels["animal_type"] = {
|
||||||
|
"data": {
|
||||||
|
"id": str(source_data["animal_type_uuid"]),
|
||||||
|
"type": "taxonomy_term--animal_type",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload["attributes"].update(attrs)
|
||||||
|
if rels:
|
||||||
|
payload.setdefault("relationships", {}).update(rels)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class AnimalTypeImporter(ToFarmOS):
|
||||||
|
|
||||||
|
model_title = "AnimalType"
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_target_objects(self, **kwargs):
|
||||||
|
result = self.farmos_client.resource.get("taxonomy_term", "animal_type")
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
# fetch from cache, if applicable
|
||||||
|
if self.caches_target:
|
||||||
|
return super().get_target_object(key)
|
||||||
|
|
||||||
|
# okay now must fetch via API
|
||||||
|
if self.get_keys() != ["uuid"]:
|
||||||
|
raise ValueError("must use uuid key for this to work")
|
||||||
|
uuid = key[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.farmos_client.resource.get_id(
|
||||||
|
"taxonomy_term", "animal_type", str(uuid)
|
||||||
|
)
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
def normalize_target_object(self, obj):
|
||||||
|
return {
|
||||||
|
"uuid": UUID(obj["id"]),
|
||||||
|
"name": obj["attributes"]["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_type_payload(self, source_data):
|
||||||
|
return {
|
||||||
|
"attributes": {
|
||||||
|
"name": source_data["name"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_target_object(self, key, source_data):
|
||||||
|
if source_data.get("__ignoreme__"):
|
||||||
|
return None
|
||||||
|
if self.dry_run:
|
||||||
|
return source_data
|
||||||
|
|
||||||
|
payload = self.get_type_payload(source_data)
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"taxonomy_term", "animal_type", payload
|
||||||
|
)
|
||||||
|
normal = self.normalize_target_object(result["data"])
|
||||||
|
normal["_new_object"] = result["data"]
|
||||||
|
return normal
|
||||||
|
|
||||||
|
def update_target_object(self, asset, source_data, target_data=None):
|
||||||
|
if self.dry_run:
|
||||||
|
return asset
|
||||||
|
|
||||||
|
payload = self.get_type_payload(source_data)
|
||||||
|
payload["id"] = str(source_data["uuid"])
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"taxonomy_term", "animal_type", payload
|
||||||
|
)
|
||||||
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
|
model_title = "GroupAsset"
|
||||||
|
farmos_asset_type = "group"
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LandAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
|
model_title = "LandAsset"
|
||||||
|
farmos_asset_type = "land"
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"land_type_id",
|
||||||
|
"is_location",
|
||||||
|
"is_fixed",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_target_object(self, land):
|
||||||
|
data = super().normalize_target_object(land)
|
||||||
|
data.update(
|
||||||
|
{
|
||||||
|
"land_type_id": land["attributes"]["land_type"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_asset_payload(self, source_data):
|
||||||
|
payload = super().get_asset_payload(source_data)
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if "land_type_id" in self.fields:
|
||||||
|
attrs["land_type"] = source_data["land_type_id"]
|
||||||
|
|
||||||
|
if attrs:
|
||||||
|
payload["attributes"].update(attrs)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class StructureAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
|
model_title = "StructureAsset"
|
||||||
|
farmos_asset_type = "structure"
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"structure_type_id",
|
||||||
|
"is_location",
|
||||||
|
"is_fixed",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_target_object(self, structure):
|
||||||
|
data = super().normalize_target_object(structure)
|
||||||
|
data.update(
|
||||||
|
{
|
||||||
|
"structure_type_id": structure["attributes"]["structure_type"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_asset_payload(self, source_data):
|
||||||
|
payload = super().get_asset_payload(source_data)
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if "structure_type_id" in self.fields:
|
||||||
|
attrs["structure_type"] = source_data["structure_type_id"]
|
||||||
|
|
||||||
|
if attrs:
|
||||||
|
payload["attributes"].update(attrs)
|
||||||
|
|
||||||
|
return payload
|
||||||
263
src/wuttafarm/farmos/importing/wuttafarm.py
Normal file
263
src/wuttafarm/farmos/importing/wuttafarm.py
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS data export
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oauthlib.oauth2 import BackendApplicationClient
|
||||||
|
from requests_oauthlib import OAuth2Session
|
||||||
|
|
||||||
|
from wuttasync.importing import ImportHandler, FromWuttaHandler, FromWutta, Orientation
|
||||||
|
|
||||||
|
from wuttafarm.db import model
|
||||||
|
from wuttafarm.farmos import importing as farmos_importing
|
||||||
|
|
||||||
|
|
||||||
|
class FromWuttaFarmHandler(FromWuttaHandler):
|
||||||
|
"""
|
||||||
|
Base class for import handler targeting WuttaFarm
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_key = "wuttafarm"
|
||||||
|
|
||||||
|
|
||||||
|
class ToFarmOSHandler(ImportHandler):
|
||||||
|
"""
|
||||||
|
Base class for export handlers using CSV file(s) as data target.
|
||||||
|
"""
|
||||||
|
|
||||||
|
target_key = "farmos"
|
||||||
|
generic_target_title = "farmOS"
|
||||||
|
|
||||||
|
# TODO: a lot of duplication to cleanup here; see FromFarmOSHandler
|
||||||
|
|
||||||
|
def begin_target_transaction(self):
|
||||||
|
"""
|
||||||
|
Establish the farmOS API client.
|
||||||
|
"""
|
||||||
|
token = self.get_farmos_oauth2_token()
|
||||||
|
self.farmos_client = self.app.get_farmos_client(token=token)
|
||||||
|
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_importer_kwargs(self, key, **kwargs):
|
||||||
|
kwargs = super().get_importer_kwargs(key, **kwargs)
|
||||||
|
kwargs["farmos_client"] = self.farmos_client
|
||||||
|
kwargs["farmos_4x"] = self.farmos_4x
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
||||||
|
"""
|
||||||
|
Handler for WuttaFarm → farmOS API export.
|
||||||
|
"""
|
||||||
|
|
||||||
|
orientation = Orientation.EXPORT
|
||||||
|
|
||||||
|
def define_importers(self):
|
||||||
|
""" """
|
||||||
|
importers = super().define_importers()
|
||||||
|
importers["LandAsset"] = LandAssetImporter
|
||||||
|
importers["StructureAsset"] = StructureAssetImporter
|
||||||
|
importers["AnimalType"] = AnimalTypeImporter
|
||||||
|
importers["AnimalAsset"] = AnimalAssetImporter
|
||||||
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
|
return importers
|
||||||
|
|
||||||
|
|
||||||
|
class FromWuttaFarm(FromWutta):
|
||||||
|
|
||||||
|
drupal_internal_id_field = "drupal_internal__id"
|
||||||
|
|
||||||
|
def create_target_object(self, key, source_data):
|
||||||
|
obj = super().create_target_object(key, source_data)
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self.dry_run:
|
||||||
|
|
||||||
|
# set farmOS, Drupal key fields in WuttaFarm
|
||||||
|
api_object = obj["_new_object"]
|
||||||
|
wf_object = source_data["_src_object"]
|
||||||
|
wf_object.farmos_uuid = obj["uuid"]
|
||||||
|
wf_object.drupal_id = api_object["attributes"][
|
||||||
|
self.drupal_internal_id_field
|
||||||
|
]
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class AnimalAssetImporter(FromWuttaFarm, farmos_importing.model.AnimalAssetImporter):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Animal Assets
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.AnimalAsset
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"animal_type_uuid",
|
||||||
|
"sex",
|
||||||
|
"is_sterile",
|
||||||
|
"birthdate",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_source_object(self, animal):
|
||||||
|
return {
|
||||||
|
"uuid": animal.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"asset_name": animal.asset_name,
|
||||||
|
"animal_type_uuid": animal.animal_type.farmos_uuid,
|
||||||
|
"sex": animal.sex,
|
||||||
|
"is_sterile": animal.is_sterile,
|
||||||
|
"birthdate": animal.birthdate,
|
||||||
|
"notes": animal.notes,
|
||||||
|
"archived": animal.archived,
|
||||||
|
"_src_object": animal,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporter):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Animal Types
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.AnimalType
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
drupal_internal_id_field = "drupal_internal__tid"
|
||||||
|
|
||||||
|
def normalize_source_object(self, animal_type):
|
||||||
|
return {
|
||||||
|
"uuid": animal_type.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"name": animal_type.name,
|
||||||
|
"_src_object": animal_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Group Assets
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.GroupAsset
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_source_object(self, group):
|
||||||
|
return {
|
||||||
|
"uuid": group.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"asset_name": group.asset_name,
|
||||||
|
"notes": group.notes,
|
||||||
|
"archived": group.archived,
|
||||||
|
"_src_object": group,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Land Assets
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.LandAsset
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"land_type_id",
|
||||||
|
"is_location",
|
||||||
|
"is_fixed",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_source_object(self, land):
|
||||||
|
return {
|
||||||
|
"uuid": land.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"asset_name": land.asset_name,
|
||||||
|
"land_type_id": land.land_type.drupal_id,
|
||||||
|
"is_location": land.is_location,
|
||||||
|
"is_fixed": land.is_fixed,
|
||||||
|
"notes": land.notes,
|
||||||
|
"archived": land.archived,
|
||||||
|
"_src_object": land,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StructureAssetImporter(
|
||||||
|
FromWuttaFarm, farmos_importing.model.StructureAssetImporter
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Structure Assets
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.StructureAsset
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"asset_name",
|
||||||
|
"structure_type_id",
|
||||||
|
"is_location",
|
||||||
|
"is_fixed",
|
||||||
|
"notes",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_source_object(self, structure):
|
||||||
|
return {
|
||||||
|
"uuid": structure.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"asset_name": structure.asset_name,
|
||||||
|
"structure_type_id": structure.structure_type.drupal_id,
|
||||||
|
"is_location": structure.is_location,
|
||||||
|
"is_fixed": structure.is_fixed,
|
||||||
|
"notes": structure.notes,
|
||||||
|
"archived": structure.archived,
|
||||||
|
"_src_object": structure,
|
||||||
|
}
|
||||||
|
|
@ -26,13 +26,12 @@ Master view for Animals
|
||||||
from wuttaweb.forms.schema import WuttaDictEnum
|
from wuttaweb.forms.schema import WuttaDictEnum
|
||||||
|
|
||||||
from wuttafarm.db.model import AnimalType, AnimalAsset
|
from wuttafarm.db.model import AnimalType, AnimalAsset
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
||||||
from wuttafarm.web.views.assets import AssetMasterView
|
|
||||||
from wuttafarm.web.forms.schema import AnimalTypeRef
|
from wuttafarm.web.forms.schema import AnimalTypeRef
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
|
|
||||||
|
|
||||||
class AnimalTypeView(WuttaFarmMasterView):
|
class AnimalTypeView(AssetTypeMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Animal Types
|
Master view for Animal Types
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,16 @@ class AssetView(WuttaFarmMasterView):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AssetTypeMasterView(WuttaFarmMasterView):
|
||||||
|
"""
|
||||||
|
Base class for "Asset Type" master views.
|
||||||
|
|
||||||
|
A bit of a misnmer perhaps, this is *not* for the actual AssetType
|
||||||
|
model, but rather the "secondary" types, e.g. AnimalType,
|
||||||
|
LandType etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class AssetMasterView(WuttaFarmMasterView):
|
class AssetMasterView(WuttaFarmMasterView):
|
||||||
"""
|
"""
|
||||||
Base class for Asset master views
|
Base class for Asset master views
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,13 @@ class CommonView(base.CommonView):
|
||||||
"activity_logs.list",
|
"activity_logs.list",
|
||||||
"activity_logs.view",
|
"activity_logs.view",
|
||||||
"activity_logs.versions",
|
"activity_logs.versions",
|
||||||
|
"animal_types.create",
|
||||||
|
"animal_types.edit",
|
||||||
"animal_types.list",
|
"animal_types.list",
|
||||||
"animal_types.view",
|
"animal_types.view",
|
||||||
"animal_types.versions",
|
"animal_types.versions",
|
||||||
|
"animal_assets.create",
|
||||||
|
"animal_assets.edit",
|
||||||
"animal_assets.list",
|
"animal_assets.list",
|
||||||
"animal_assets.view",
|
"animal_assets.view",
|
||||||
"animal_assets.versions",
|
"animal_assets.versions",
|
||||||
|
|
@ -86,9 +90,13 @@ class CommonView(base.CommonView):
|
||||||
"farmos_structures.view",
|
"farmos_structures.view",
|
||||||
"farmos_users.list",
|
"farmos_users.list",
|
||||||
"farmos_users.view",
|
"farmos_users.view",
|
||||||
|
"group_asests.create",
|
||||||
|
"group_asests.edit",
|
||||||
"group_asests.list",
|
"group_asests.list",
|
||||||
"group_asests.view",
|
"group_asests.view",
|
||||||
"group_asests.versions",
|
"group_asests.versions",
|
||||||
|
"land_assets.create",
|
||||||
|
"land_assets.edit",
|
||||||
"land_assets.list",
|
"land_assets.list",
|
||||||
"land_assets.view",
|
"land_assets.view",
|
||||||
"land_assets.versions",
|
"land_assets.versions",
|
||||||
|
|
@ -101,6 +109,8 @@ class CommonView(base.CommonView):
|
||||||
"structure_types.list",
|
"structure_types.list",
|
||||||
"structure_types.view",
|
"structure_types.view",
|
||||||
"structure_types.versions",
|
"structure_types.versions",
|
||||||
|
"structure_assets.create",
|
||||||
|
"structure_assets.edit",
|
||||||
"structure_assets.list",
|
"structure_assets.list",
|
||||||
"structure_assets.view",
|
"structure_assets.view",
|
||||||
"structure_assets.versions",
|
"structure_assets.versions",
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,11 @@ Master view for Land Types
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from wuttafarm.db.model.land import LandType, LandAsset
|
from wuttafarm.db.model.land import LandType, LandAsset
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
||||||
from wuttafarm.web.views.assets import AssetMasterView
|
|
||||||
from wuttafarm.web.forms.schema import LandTypeRef
|
from wuttafarm.web.forms.schema import LandTypeRef
|
||||||
|
|
||||||
|
|
||||||
class LandTypeView(WuttaFarmMasterView):
|
class LandTypeView(AssetTypeMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Land Types
|
Master view for Land Types
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -96,3 +96,7 @@ class WuttaFarmMasterView(MasterView):
|
||||||
f.remove("drupal_id")
|
f.remove("drupal_id")
|
||||||
else:
|
else:
|
||||||
f.set_readonly("drupal_id")
|
f.set_readonly("drupal_id")
|
||||||
|
|
||||||
|
def persist(self, obj, session=None):
|
||||||
|
super().persist(obj, session)
|
||||||
|
self.app.export_to_farmos(obj, require=False)
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,13 @@
|
||||||
Master view for Structures
|
Master view for Structures
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
||||||
from wuttafarm.web.views.assets import AssetMasterView
|
|
||||||
from wuttafarm.db.model import StructureType, StructureAsset
|
from wuttafarm.db.model import StructureType, StructureAsset
|
||||||
from wuttafarm.web.forms.schema import StructureTypeRef
|
from wuttafarm.web.forms.schema import StructureTypeRef
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
|
|
||||||
|
|
||||||
class StructureTypeView(WuttaFarmMasterView):
|
class StructureTypeView(AssetTypeMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Structure Types
|
Master view for Structure Types
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue