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:
Lance Edgar 2026-02-16 14:53:36 -06:00
parent 6677fe1e23
commit da9b559752
14 changed files with 765 additions and 9 deletions

View file

@ -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"

View file

@ -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):
""" """

View file

@ -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

View 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)

View file

@ -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.

View 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

View 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

View 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,
}

View file

@ -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
""" """

View file

@ -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

View file

@ -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",

View file

@ -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
""" """

View file

@ -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)

View file

@ -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
""" """