diff --git a/pyproject.toml b/pyproject.toml
index 073879b..51dcb61 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -58,6 +58,7 @@ wuttafarm = "wuttafarm.app:WuttaFarmAppProvider"
"wuttafarm" = "wuttafarm.web.menus:WuttaFarmMenuHandler"
[project.entry-points."wuttasync.importing"]
+"export.to_farmos.from_wuttafarm" = "wuttafarm.farmos.importing.wuttafarm:FromWuttaFarmToFarmOS"
"import.to_wuttafarm.from_farmos" = "wuttafarm.importing.farmos:FromFarmOSToWuttaFarm"
diff --git a/src/wuttafarm/app.py b/src/wuttafarm/app.py
index 72e9f00..087c48a 100644
--- a/src/wuttafarm/app.py
+++ b/src/wuttafarm/app.py
@@ -85,6 +85,38 @@ class WuttaFarmAppHandler(base.AppHandler):
handler = self.get_farmos_handler()
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):
"""
diff --git a/src/wuttafarm/cli/__init__.py b/src/wuttafarm/cli/__init__.py
index 7f6c2bb..cd06344 100644
--- a/src/wuttafarm/cli/__init__.py
+++ b/src/wuttafarm/cli/__init__.py
@@ -26,5 +26,6 @@ WuttaFarm CLI
from .base import wuttafarm_typer
# nb. must bring in all modules for discovery to work
+from . import export_farmos
from . import import_farmos
from . import install
diff --git a/src/wuttafarm/cli/export_farmos.py b/src/wuttafarm/cli/export_farmos.py
new file mode 100644
index 0000000..18a21dd
--- /dev/null
+++ b/src/wuttafarm/cli/export_farmos.py
@@ -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 .
+#
+################################################################################
+"""
+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)
diff --git a/src/wuttafarm/emails.py b/src/wuttafarm/emails.py
index 55b1612..05416ab 100644
--- a/src/wuttafarm/emails.py
+++ b/src/wuttafarm/emails.py
@@ -26,6 +26,12 @@ Email sending config for WuttaFarm
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):
"""
Diff warning for farmOS → WuttaFarm import.
diff --git a/src/wuttafarm/farmos/importing/__init__.py b/src/wuttafarm/farmos/importing/__init__.py
new file mode 100644
index 0000000..a4b17eb
--- /dev/null
+++ b/src/wuttafarm/farmos/importing/__init__.py
@@ -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 .
+#
+################################################################################
+"""
+Importing data *into* farmOS
+"""
+
+from . import model
diff --git a/src/wuttafarm/farmos/importing/model.py b/src/wuttafarm/farmos/importing/model.py
new file mode 100644
index 0000000..6c3f5a0
--- /dev/null
+++ b/src/wuttafarm/farmos/importing/model.py
@@ -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 .
+#
+################################################################################
+"""
+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
diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py
new file mode 100644
index 0000000..8ef8a77
--- /dev/null
+++ b/src/wuttafarm/farmos/importing/wuttafarm.py
@@ -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 .
+#
+################################################################################
+"""
+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,
+ }
diff --git a/src/wuttafarm/web/views/animals.py b/src/wuttafarm/web/views/animals.py
index fbad4ce..bae7dde 100644
--- a/src/wuttafarm/web/views/animals.py
+++ b/src/wuttafarm/web/views/animals.py
@@ -26,13 +26,12 @@ Master view for Animals
from wuttaweb.forms.schema import WuttaDictEnum
from wuttafarm.db.model import AnimalType, AnimalAsset
-from wuttafarm.web.views import WuttaFarmMasterView
-from wuttafarm.web.views.assets import AssetMasterView
+from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
from wuttafarm.web.forms.schema import AnimalTypeRef
from wuttafarm.web.forms.widgets import ImageWidget
-class AnimalTypeView(WuttaFarmMasterView):
+class AnimalTypeView(AssetTypeMasterView):
"""
Master view for Animal Types
"""
diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py
index f0ebefe..dffaae7 100644
--- a/src/wuttafarm/web/views/assets.py
+++ b/src/wuttafarm/web/views/assets.py
@@ -118,6 +118,16 @@ class AssetView(WuttaFarmMasterView):
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):
"""
Base class for Asset master views
diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py
index 8b030f5..121e631 100644
--- a/src/wuttafarm/web/views/common.py
+++ b/src/wuttafarm/web/views/common.py
@@ -54,9 +54,13 @@ class CommonView(base.CommonView):
"activity_logs.list",
"activity_logs.view",
"activity_logs.versions",
+ "animal_types.create",
+ "animal_types.edit",
"animal_types.list",
"animal_types.view",
"animal_types.versions",
+ "animal_assets.create",
+ "animal_assets.edit",
"animal_assets.list",
"animal_assets.view",
"animal_assets.versions",
@@ -86,9 +90,13 @@ class CommonView(base.CommonView):
"farmos_structures.view",
"farmos_users.list",
"farmos_users.view",
+ "group_asests.create",
+ "group_asests.edit",
"group_asests.list",
"group_asests.view",
"group_asests.versions",
+ "land_assets.create",
+ "land_assets.edit",
"land_assets.list",
"land_assets.view",
"land_assets.versions",
@@ -101,6 +109,8 @@ class CommonView(base.CommonView):
"structure_types.list",
"structure_types.view",
"structure_types.versions",
+ "structure_assets.create",
+ "structure_assets.edit",
"structure_assets.list",
"structure_assets.view",
"structure_assets.versions",
diff --git a/src/wuttafarm/web/views/land.py b/src/wuttafarm/web/views/land.py
index ce577c9..aad15e7 100644
--- a/src/wuttafarm/web/views/land.py
+++ b/src/wuttafarm/web/views/land.py
@@ -26,12 +26,11 @@ Master view for Land Types
from webhelpers2.html import HTML, tags
from wuttafarm.db.model.land import LandType, LandAsset
-from wuttafarm.web.views import WuttaFarmMasterView
-from wuttafarm.web.views.assets import AssetMasterView
+from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
from wuttafarm.web.forms.schema import LandTypeRef
-class LandTypeView(WuttaFarmMasterView):
+class LandTypeView(AssetTypeMasterView):
"""
Master view for Land Types
"""
diff --git a/src/wuttafarm/web/views/master.py b/src/wuttafarm/web/views/master.py
index 98856f6..0e25a30 100644
--- a/src/wuttafarm/web/views/master.py
+++ b/src/wuttafarm/web/views/master.py
@@ -96,3 +96,7 @@ class WuttaFarmMasterView(MasterView):
f.remove("drupal_id")
else:
f.set_readonly("drupal_id")
+
+ def persist(self, obj, session=None):
+ super().persist(obj, session)
+ self.app.export_to_farmos(obj, require=False)
diff --git a/src/wuttafarm/web/views/structures.py b/src/wuttafarm/web/views/structures.py
index 5745658..aa9bf31 100644
--- a/src/wuttafarm/web/views/structures.py
+++ b/src/wuttafarm/web/views/structures.py
@@ -23,14 +23,13 @@
Master view for Structures
"""
-from wuttafarm.web.views import WuttaFarmMasterView
-from wuttafarm.web.views.assets import AssetMasterView
+from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
from wuttafarm.db.model import StructureType, StructureAsset
from wuttafarm.web.forms.schema import StructureTypeRef
from wuttafarm.web.forms.widgets import ImageWidget
-class StructureTypeView(WuttaFarmMasterView):
+class StructureTypeView(AssetTypeMasterView):
"""
Master view for Structure Types
"""