feat: add edit/sync support for asset parents

plus several changes to API calls, use iterate() instead of get()

also some changes to better share code for asset importers
This commit is contained in:
Lance Edgar 2026-03-06 19:52:20 -06:00
parent d46ba43d11
commit e61043b9d9
11 changed files with 437 additions and 293 deletions

View file

@ -74,10 +74,11 @@ class ToFarmOSTaxonomy(ToFarmOS):
] ]
def get_target_objects(self, **kwargs): def get_target_objects(self, **kwargs):
result = self.farmos_client.resource.get( return list(
self.farmos_client.resource.iterate(
"taxonomy_term", self.farmos_taxonomy_type "taxonomy_term", self.farmos_taxonomy_type
) )
return result["data"] )
def get_target_object(self, key): def get_target_object(self, key):
@ -127,9 +128,9 @@ class ToFarmOSTaxonomy(ToFarmOS):
normal["_new_object"] = result["data"] normal["_new_object"] = result["data"]
return normal return normal
def update_target_object(self, asset, source_data, target_data=None): def update_target_object(self, term, source_data, target_data=None):
if self.dry_run: if self.dry_run:
return asset return term
payload = self.get_term_payload(source_data) payload = self.get_term_payload(source_data)
payload["id"] = str(source_data["uuid"]) payload["id"] = str(source_data["uuid"])
@ -146,9 +147,12 @@ class ToFarmOSAsset(ToFarmOS):
farmos_asset_type = None farmos_asset_type = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.normal = self.app.get_normalizer(self.farmos_client)
def get_target_objects(self, **kwargs): def get_target_objects(self, **kwargs):
assets = self.farmos_client.asset.get(self.farmos_asset_type) return list(self.farmos_client.asset.iterate(self.farmos_asset_type))
return assets["data"]
def get_target_object(self, key): def get_target_object(self, key):
@ -191,18 +195,17 @@ class ToFarmOSAsset(ToFarmOS):
return self.normalize_target_object(result["data"]) return self.normalize_target_object(result["data"])
def normalize_target_object(self, asset): def normalize_target_object(self, asset):
normal = self.normal.normalize_farmos_asset(asset)
if notes := asset["attributes"]["notes"]:
notes = notes["value"]
return { return {
"uuid": UUID(asset["id"]), "uuid": UUID(normal["uuid"]),
"asset_name": asset["attributes"]["name"], "asset_name": normal["asset_name"],
"is_location": asset["attributes"]["is_location"], "is_location": normal["is_location"],
"is_fixed": asset["attributes"]["is_fixed"], "is_fixed": normal["is_fixed"],
"produces_eggs": asset["attributes"].get("produces_eggs"), # nb. this is only used for certain asset types
"notes": notes, "produces_eggs": normal["produces_eggs"],
"archived": asset["attributes"]["archived"], "parents": [(p["asset_type"], UUID(p["uuid"])) for p in normal["parents"]],
"notes": normal["notes"],
"archived": normal["archived"],
} }
def get_asset_payload(self, source_data): def get_asset_payload(self, source_data):
@ -221,8 +224,18 @@ class ToFarmOSAsset(ToFarmOS):
if "archived" in self.fields: if "archived" in self.fields:
attrs["archived"] = source_data["archived"] attrs["archived"] = source_data["archived"]
payload = {"attributes": attrs} rels = {}
if "parents" in self.fields:
rels["parent"] = {"data": []}
for asset_type, uuid in source_data["parents"]:
rels["parent"]["data"].append(
{
"id": str(uuid),
"type": f"asset--{asset_type}",
}
)
payload = {"attributes": attrs, "relationships": rels}
return payload return payload
@ -607,8 +620,7 @@ class ToFarmOSLog(ToFarmOS):
self.normal = self.app.get_normalizer(self.farmos_client) self.normal = self.app.get_normalizer(self.farmos_client)
def get_target_objects(self, **kwargs): def get_target_objects(self, **kwargs):
result = self.farmos_client.log.get(self.farmos_log_type) return list(self.farmos_client.log.iterate(self.farmos_log_type))
return result["data"]
def get_target_object(self, key): def get_target_object(self, key):

View file

@ -134,42 +134,68 @@ class FromWuttaFarm(FromWutta):
return obj return obj
class AnimalAssetImporter(FromWuttaFarm, farmos_importing.model.AnimalAssetImporter): class FromWuttaFarmAsset(FromWuttaFarm):
"""
Base class for WuttaFarm farmOS API asset exporters
"""
supported_fields = [
"uuid",
"asset_name",
"is_location",
"is_fixed",
"parents",
"notes",
"archived",
]
def normalize_source_object(self, asset):
return {
"uuid": asset.farmos_uuid or self.app.make_true_uuid(),
"asset_name": asset.asset_name,
"is_location": asset.is_location,
"is_fixed": asset.is_fixed,
"parents": [(p.asset_type, p.farmos_uuid) for p in asset.parents],
"notes": asset.notes,
"archived": asset.archived,
"_src_object": asset,
}
class AnimalAssetImporter(
FromWuttaFarmAsset, farmos_importing.model.AnimalAssetImporter
):
""" """
WuttaFarm farmOS API exporter for Animal Assets WuttaFarm farmOS API exporter for Animal Assets
""" """
source_model_class = model.AnimalAsset source_model_class = model.AnimalAsset
supported_fields = [ def get_supported_fields(self):
"uuid", fields = list(super().get_supported_fields())
"asset_name", fields.extend(
[
"animal_type_uuid", "animal_type_uuid",
"sex", "sex",
"is_sterile", "is_sterile",
"produces_eggs", "produces_eggs",
"birthdate", "birthdate",
"is_location",
"is_fixed",
"notes",
"archived",
] ]
)
return fields
def normalize_source_object(self, animal): def normalize_source_object(self, animal):
return { data = super().normalize_source_object(animal)
"uuid": animal.farmos_uuid or self.app.make_true_uuid(), data.update(
"asset_name": animal.asset_name, {
"animal_type_uuid": animal.animal_type.farmos_uuid, "animal_type_uuid": animal.animal_type.farmos_uuid,
"sex": animal.sex, "sex": animal.sex,
"is_sterile": animal.is_sterile, "is_sterile": animal.is_sterile,
"produces_eggs": animal.produces_eggs, "produces_eggs": animal.produces_eggs,
"birthdate": animal.birthdate, "birthdate": animal.birthdate,
"is_location": animal.is_location,
"is_fixed": animal.is_fixed,
"notes": animal.notes,
"archived": animal.archived,
"_src_object": animal,
} }
)
return data
class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporter): class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporter):
@ -216,60 +242,56 @@ class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter):
} }
class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter): class GroupAssetImporter(FromWuttaFarmAsset, farmos_importing.model.GroupAssetImporter):
""" """
WuttaFarm farmOS API exporter for Group Assets WuttaFarm farmOS API exporter for Group Assets
""" """
source_model_class = model.GroupAsset source_model_class = model.GroupAsset
supported_fields = [ def get_supported_fields(self):
"uuid", fields = list(super().get_supported_fields())
"asset_name", fields.extend(
[
"produces_eggs", "produces_eggs",
"notes",
"archived",
] ]
)
return fields
def normalize_source_object(self, group): def normalize_source_object(self, group):
return { data = super().normalize_source_object(group)
"uuid": group.farmos_uuid or self.app.make_true_uuid(), data.update(
"asset_name": group.asset_name, {
"produces_eggs": group.produces_eggs, "produces_eggs": group.produces_eggs,
"notes": group.notes,
"archived": group.archived,
"_src_object": group,
} }
)
return data
class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter): class LandAssetImporter(FromWuttaFarmAsset, farmos_importing.model.LandAssetImporter):
""" """
WuttaFarm farmOS API exporter for Land Assets WuttaFarm farmOS API exporter for Land Assets
""" """
source_model_class = model.LandAsset source_model_class = model.LandAsset
supported_fields = [ def get_supported_fields(self):
"uuid", fields = list(super().get_supported_fields())
"asset_name", fields.extend(
[
"land_type_id", "land_type_id",
"is_location",
"is_fixed",
"notes",
"archived",
] ]
)
return fields
def normalize_source_object(self, land): def normalize_source_object(self, land):
return { data = super().normalize_source_object(land)
"uuid": land.farmos_uuid or self.app.make_true_uuid(), data.update(
"asset_name": land.asset_name, {
"land_type_id": land.land_type.drupal_id, "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,
} }
)
return data
class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter): class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter):
@ -294,34 +316,36 @@ class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter)
} }
class PlantAssetImporter(FromWuttaFarm, farmos_importing.model.PlantAssetImporter): class PlantAssetImporter(FromWuttaFarmAsset, farmos_importing.model.PlantAssetImporter):
""" """
WuttaFarm farmOS API exporter for Plant Assets WuttaFarm farmOS API exporter for Plant Assets
""" """
source_model_class = model.PlantAsset source_model_class = model.PlantAsset
supported_fields = [ def get_supported_fields(self):
"uuid", fields = list(super().get_supported_fields())
"asset_name", fields.extend(
[
"plant_type_uuids", "plant_type_uuids",
"notes",
"archived",
] ]
)
return fields
def normalize_source_object(self, plant): def normalize_source_object(self, plant):
return { data = super().normalize_source_object(plant)
"uuid": plant.farmos_uuid or self.app.make_true_uuid(), data.update(
"asset_name": plant.asset_name, {
"plant_type_uuids": [t.plant_type.farmos_uuid for t in plant._plant_types], "plant_type_uuids": [
"notes": plant.notes, t.plant_type.farmos_uuid for t in plant._plant_types
"archived": plant.archived, ],
"_src_object": plant,
} }
)
return data
class StructureAssetImporter( class StructureAssetImporter(
FromWuttaFarm, farmos_importing.model.StructureAssetImporter FromWuttaFarmAsset, farmos_importing.model.StructureAssetImporter
): ):
""" """
WuttaFarm farmOS API exporter for Structure Assets WuttaFarm farmOS API exporter for Structure Assets
@ -329,27 +353,23 @@ class StructureAssetImporter(
source_model_class = model.StructureAsset source_model_class = model.StructureAsset
supported_fields = [ def get_supported_fields(self):
"uuid", fields = list(super().get_supported_fields())
"asset_name", fields.extend(
[
"structure_type_id", "structure_type_id",
"is_location",
"is_fixed",
"notes",
"archived",
] ]
)
return fields
def normalize_source_object(self, structure): def normalize_source_object(self, structure):
return { data = super().normalize_source_object(structure)
"uuid": structure.farmos_uuid or self.app.make_true_uuid(), data.update(
"asset_name": structure.asset_name, {
"structure_type_id": structure.structure_type.drupal_id, "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,
} }
)
return data
############################## ##############################

View file

@ -330,21 +330,18 @@ class AnimalAssetImporter(AssetImporterBase):
model_class = model.AnimalAsset model_class = model.AnimalAsset
supported_fields = [ def get_supported_fields(self):
"farmos_uuid", fields = list(super().get_supported_fields())
"drupal_id", fields.extend(
"asset_type", [
"asset_name",
"animal_type_uuid", "animal_type_uuid",
"sex", "sex",
"is_sterile", "is_sterile",
"produces_eggs", "produces_eggs",
"birthdate", "birthdate",
"notes",
"archived",
"image_url",
"thumbnail_url",
] ]
)
return fields
def setup(self): def setup(self):
super().setup() super().setup()
@ -415,8 +412,7 @@ class AnimalTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
animal_types = self.farmos_client.resource.get("taxonomy_term", "animal_type") return list(self.farmos_client.resource.iterate("taxonomy_term", "animal_type"))
return animal_types["data"]
def normalize_source_object(self, animal_type): def normalize_source_object(self, animal_type):
""" """ """ """
@ -444,8 +440,7 @@ class AssetTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
asset_types = self.farmos_client.resource.get("asset_type") return list(self.farmos_client.resource.iterate("asset_type"))
return asset_types["data"]
def normalize_source_object(self, asset_type): def normalize_source_object(self, asset_type):
""" """ """ """
@ -464,20 +459,14 @@ class GroupAssetImporter(AssetImporterBase):
model_class = model.GroupAsset model_class = model.GroupAsset
supported_fields = [ def get_supported_fields(self):
"farmos_uuid", fields = list(super().get_supported_fields())
"drupal_id", fields.extend(
"asset_type", [
"asset_name",
"is_location",
"is_fixed",
"produces_eggs", "produces_eggs",
"notes",
"archived",
"image_url",
"thumbnail_url",
"parents",
] ]
)
return fields
def normalize_source_object(self, group): def normalize_source_object(self, group):
""" """ """ """
@ -497,18 +486,14 @@ class LandAssetImporter(AssetImporterBase):
model_class = model.LandAsset model_class = model.LandAsset
supported_fields = [ def get_supported_fields(self):
"farmos_uuid", fields = list(super().get_supported_fields())
"drupal_id", fields.extend(
"asset_type", [
"asset_name",
"land_type_uuid", "land_type_uuid",
"is_location",
"is_fixed",
"notes",
"archived",
"parents",
] ]
)
return fields
def setup(self): def setup(self):
""" """ """ """
@ -553,8 +538,7 @@ class LandTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
land_types = self.farmos_client.resource.get("land_type") return list(self.farmos_client.resource.iterate("land_type"))
return land_types["data"]
def normalize_source_object(self, land_type): def normalize_source_object(self, land_type):
""" """ """ """
@ -581,8 +565,7 @@ class PlantTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
result = self.farmos_client.resource.get("taxonomy_term", "plant_type") return list(self.farmos_client.resource.iterate("taxonomy_term", "plant_type"))
return result["data"]
def normalize_source_object(self, plant_type): def normalize_source_object(self, plant_type):
""" """ """ """
@ -601,17 +584,14 @@ class PlantAssetImporter(AssetImporterBase):
model_class = model.PlantAsset model_class = model.PlantAsset
supported_fields = [ def get_supported_fields(self):
"farmos_uuid", fields = list(super().get_supported_fields())
"drupal_id", fields.extend(
"asset_type", [
"asset_name",
"plant_types", "plant_types",
"notes",
"archived",
"image_url",
"thumbnail_url",
] ]
)
return fields
def setup(self): def setup(self):
super().setup() super().setup()
@ -624,6 +604,8 @@ class PlantAssetImporter(AssetImporterBase):
def normalize_source_object(self, plant): def normalize_source_object(self, plant):
""" """ """ """
data = super().normalize_source_object(plant)
plant_types = [] plant_types = []
if relationships := plant.get("relationships"): if relationships := plant.get("relationships"):
@ -637,7 +619,6 @@ class PlantAssetImporter(AssetImporterBase):
else: else:
log.warning("plant type not found: %s", plant_type["id"]) log.warning("plant type not found: %s", plant_type["id"])
data = super().normalize_source_object(plant)
data.update( data.update(
{ {
"plant_types": set(plant_types), "plant_types": set(plant_types),
@ -693,20 +674,14 @@ class StructureAssetImporter(AssetImporterBase):
model_class = model.StructureAsset model_class = model.StructureAsset
supported_fields = [ def get_supported_fields(self):
"farmos_uuid", fields = list(super().get_supported_fields())
"drupal_id", fields.extend(
"asset_type", [
"asset_name",
"structure_type_uuid", "structure_type_uuid",
"is_location",
"is_fixed",
"notes",
"archived",
"image_url",
"thumbnail_url",
"parents",
] ]
)
return fields
def setup(self): def setup(self):
super().setup() super().setup()
@ -752,8 +727,7 @@ class StructureTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
structure_types = self.farmos_client.resource.get("structure_type") return list(self.farmos_client.resource.iterate("structure_type"))
return structure_types["data"]
def normalize_source_object(self, structure_type): def normalize_source_object(self, structure_type):
""" """ """ """
@ -791,8 +765,7 @@ class UserImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
users = self.farmos_client.resource.get("user") return list(self.farmos_client.resource.iterate("user"))
return users["data"]
def normalize_source_object(self, user): def normalize_source_object(self, user):
""" """ """ """
@ -869,8 +842,7 @@ class UnitImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
result = self.farmos_client.resource.get("taxonomy_term", "unit") return list(self.farmos_client.resource.iterate("taxonomy_term", "unit"))
return result["data"]
def normalize_source_object(self, unit): def normalize_source_object(self, unit):
""" """ """ """
@ -898,8 +870,7 @@ class QuantityTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
result = self.farmos_client.resource.get("quantity_type") return list(self.farmos_client.resource.iterate("quantity_type"))
return result["data"]
def normalize_source_object(self, quantity_type): def normalize_source_object(self, quantity_type):
""" """ """ """
@ -927,8 +898,7 @@ class LogTypeImporter(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
log_types = self.farmos_client.resource.get("log_type") return list(self.farmos_client.resource.iterate("log_type"))
return log_types["data"]
def normalize_source_object(self, log_type): def normalize_source_object(self, log_type):
""" """ """ """
@ -1271,8 +1241,7 @@ class QuantityImporterBase(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
quantity_type = self.get_farmos_quantity_type() quantity_type = self.get_farmos_quantity_type()
result = self.farmos_client.resource.get("quantity", quantity_type) return list(self.farmos_client.resource.iterate("quantity", quantity_type))
return result["data"]
def get_quantity_type_by_farmos_uuid(self, uuid): def get_quantity_type_by_farmos_uuid(self, uuid):
if hasattr(self, "quantity_types_by_farmos_uuid"): if hasattr(self, "quantity_types_by_farmos_uuid"):

View file

@ -90,10 +90,29 @@ class Normalizer(GenericHandler):
if notes := asset["attributes"]["notes"]: if notes := asset["attributes"]["notes"]:
notes = notes["value"] notes = notes["value"]
parent_objects = []
parent_uuids = []
owner_objects = [] owner_objects = []
owner_uuids = [] owner_uuids = []
if relationships := asset.get("relationships"): if relationships := asset.get("relationships"):
if parents := relationships.get("parent"):
for parent in parents["data"]:
parent_uuid = parent["id"]
parent_uuids.append(parent_uuid)
parent_object = {
"uuid": parent_uuid,
"type": parent["type"],
"asset_type": parent["type"].split("--")[1],
}
if parent := included.get(parent_uuid):
parent_object.update(
{
"name": parent["attributes"]["name"],
}
)
parent_objects.append(parent_object)
if owners := relationships.get("owner"): if owners := relationships.get("owner"):
for user in owners["data"]: for user in owners["data"]:
user_uuid = user["id"] user_uuid = user["id"]
@ -114,6 +133,10 @@ class Normalizer(GenericHandler):
"is_fixed": asset["attributes"]["is_fixed"], "is_fixed": asset["attributes"]["is_fixed"],
"archived": asset["attributes"]["archived"], "archived": asset["attributes"]["archived"],
"notes": notes, "notes": notes,
# nb. this is only used for certain asset types
"produces_eggs": asset["attributes"].get("produces_eggs"),
"parents": parent_objects,
"parent_uuids": parent_uuids,
"owners": owner_objects, "owners": owner_objects,
"owner_uuids": owner_uuids, "owner_uuids": owner_uuids,
} }

View file

@ -372,37 +372,35 @@ class UsersType(colander.SchemaType):
return UsersWidget(self.request, **kwargs) return UsersWidget(self.request, **kwargs)
class AssetParentRefs(WuttaSet):
"""
Schema type for Parents field which references assets.
"""
def serialize(self, node, appstruct):
if not appstruct:
appstruct = []
uuids = [u.hex for u in appstruct]
return json.dumps(uuids)
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import AssetParentRefsWidget
return AssetParentRefsWidget(self.request, **kwargs)
class AssetRefs(WuttaSet): class AssetRefs(WuttaSet):
""" """
Schema type for Assets field (on a Log record) Schema type for Assets field (on a Log record)
""" """
def __init__(self, request, for_asset=None, **kwargs):
super().__init__(request, **kwargs)
self.for_asset = for_asset
def serialize(self, node, appstruct): def serialize(self, node, appstruct):
if not appstruct: if not appstruct:
return colander.null return colander.null
return {asset.uuid for asset in appstruct} return {asset.uuid.hex for asset in appstruct}
def widget_maker(self, **kwargs): def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import AssetRefsWidget from wuttafarm.web.forms.widgets import AssetRefsWidget
model = self.app.model
session = Session()
if "values" not in kwargs:
query = session.query(model.Asset)
if self.for_asset:
query = query.filter(model.Asset.uuid != self.for_asset.uuid)
query = query.order_by(model.Asset.asset_name)
values = [(asset.uuid.hex, str(asset)) for asset in query]
kwargs["values"] = values
return AssetRefsWidget(self.request, **kwargs) return AssetRefsWidget(self.request, **kwargs)

View file

@ -393,42 +393,20 @@ class UsersWidget(Widget):
############################## ##############################
class AssetParentRefsWidget(WuttaCheckboxChoiceWidget): class AssetRefsWidget(Widget):
"""
Widget for Parents field which references assets.
"""
def serialize(self, field, cstruct, **kw):
""" """
model = self.app.model
session = Session()
readonly = kw.get("readonly", self.readonly)
if readonly:
parents = []
for uuid in json.loads(cstruct):
parent = session.get(model.Asset, uuid)
parents.append(
HTML.tag(
"li",
c=tags.link_to(
str(parent),
self.request.route_url(
f"{parent.asset_type}_assets.view", uuid=parent.uuid
),
),
)
)
return HTML.tag("ul", c=parents)
return super().serialize(field, cstruct, **kw)
class AssetRefsWidget(WuttaCheckboxChoiceWidget):
""" """
Widget for Assets field (of various kinds). Widget for Assets field (of various kinds).
""" """
template = "assetrefs"
values = ()
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
def serialize(self, field, cstruct, **kw): def serialize(self, field, cstruct, **kw):
""" """ """ """
model = self.app.model model = self.app.model
@ -452,7 +430,28 @@ class AssetRefsWidget(WuttaCheckboxChoiceWidget):
) )
return HTML.tag("ul", c=assets) return HTML.tag("ul", c=assets)
return super().serialize(field, cstruct, **kw) values = kw.get("values", self.values)
if not isinstance(values, sequence_types):
raise TypeError("Values must be a sequence type (list, tuple, or range).")
kw["values"] = _normalize_choices(values)
tmpl_values = self.get_template_values(field, cstruct, kw)
return field.renderer(self.template, **tmpl_values)
def get_template_values(self, field, cstruct, kw):
""" """
values = super().get_template_values(field, cstruct, kw)
values["js_values"] = json.dumps(values["values"])
return values
def deserialize(self, field, pstruct):
""" """
if not pstruct:
return colander.null
return set(pstruct.split(","))
class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget): class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget):

View file

@ -0,0 +1,11 @@
<div tal:define="
name name|field.name;
oid oid|field.oid;
vmodel vmodel|'modelData.'+oid;"
tal:omit-tag="">
<assets-picker tal:attributes="name name;
v-model vmodel;
:assets js_values;" />
</div>

View file

@ -1,9 +1,116 @@
<%def name="make_wuttafarm_components()"> <%def name="make_wuttafarm_components()">
${self.make_assets_picker_component()}
${self.make_animal_type_picker_component()} ${self.make_animal_type_picker_component()}
${self.make_plant_types_picker_component()} ${self.make_plant_types_picker_component()}
</%def> </%def>
<%def name="make_assets_picker_component()">
<script type="text/x-template" id="assets-picker-template">
<div>
<input type="hidden" :name="name" :value="value" />
<div style="display: flex; gap: 0.5rem; align-items: center;">
<span>Add:</span>
<b-autocomplete v-model="addName"
ref="addName"
:data="addNameData"
field="name"
open-on-focus
keep-first
@select="addNameSelected"
clear-on-select
style="flex-grow: 1;">
<template #empty>No results found</template>
</b-autocomplete>
</div>
<${b}-table :data="assetData">
<${b}-table-column field="name" v-slot="props">
<span>{{ props.row.name }}</span>
</${b}-table-column>
<${b}-table-column v-slot="props">
<a href="#"
class="has-text-danger"
@click.prevent="removeAsset(props.row)">
<i class="fas fa-trash" /> &nbsp; Remove
</a>
</${b}-table-column>
</${b}-table>
</div>
</script>
<script>
const AssetsPicker = {
template: '#assets-picker-template',
props: {
name: String,
value: Array,
assets: Array,
},
data() {
return {
addName: '',
internalAssets: this.assets.map((asset) => {
return {uuid: asset[0], name: asset[1]}
}),
}
},
computed: {
assetData() {
const data = []
if (this.value) {
for (let asset of this.internalAssets) {
if (this.value.includes(asset.uuid)) {
data.push(asset)
}
}
}
return data
},
addNameData() {
if (!this.addName) {
return this.internalAssets
}
return this.internalAssets.filter((asset) => {
return asset.name.toLowerCase().indexOf(this.addName.toLowerCase()) >= 0
})
},
},
methods: {
addNameSelected(option) {
const value = Array.from(this.value || [])
if (!value.includes(option.uuid)) {
value.push(option.uuid)
this.$emit('input', value)
}
this.addName = null
},
removeAsset(asset) {
let value = Array.from(this.value)
const i = value.indexOf(asset.uuid)
value.splice(i, 1)
this.$emit('input', value)
},
},
}
Vue.component('assets-picker', AssetsPicker)
<% request.register_component('assets-picker', 'AssetsPicker') %>
</script>
</%def>
<%def name="make_animal_type_picker_component()"> <%def name="make_animal_type_picker_component()">
<script type="text/x-template" id="animal-type-picker-template"> <script type="text/x-template" id="animal-type-picker-template">
<div> <div>
@ -108,7 +215,10 @@
createSave() { createSave() {
this.createSaving = true this.createSaving = true
## TODO
% if not app.is_farmos_wrapper():
const url = "${url('animal_types.ajax_create')}" const url = "${url('animal_types.ajax_create')}"
% endif
const params = {name: this.createName} const params = {name: this.createName}
this.wuttaPOST(url, params, response => { this.wuttaPOST(url, params, response => {
this.internalAnimalTypes.push([response.data.uuid, response.data.name]) this.internalAnimalTypes.push([response.data.uuid, response.data.name])
@ -229,7 +339,6 @@
return {uuid: pt[0], name: pt[1]} return {uuid: pt[0], name: pt[1]}
}), }),
addShowDialog: false,
addName: '', addName: '',
createShowDialog: false, createShowDialog: false,
@ -243,7 +352,6 @@
if (this.value) { if (this.value) {
for (let ptype of this.internalPlantTypes) { for (let ptype of this.internalPlantTypes) {
// ptype = {uuid: ptype[0], name: ptype[1]}
if (this.value.includes(ptype.uuid)) { if (this.value.includes(ptype.uuid)) {
data.push(ptype) data.push(ptype)
} }
@ -295,7 +403,10 @@
createSave() { createSave() {
this.createSaving = true this.createSaving = true
## TODO
% if not app.is_farmos_wrapper():
const url = "${url('plant_types.ajax_create')}" const url = "${url('plant_types.ajax_create')}"
% endif
const params = {name: this.createName} const params = {name: this.createName}
this.wuttaPOST(url, params, response => { this.wuttaPOST(url, params, response => {
this.internalPlantTypes.push(response.data) this.internalPlantTypes.push(response.data)

View file

@ -32,7 +32,7 @@ from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Asset, Log from wuttafarm.db.model import Asset, Log
from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs from wuttafarm.web.forms.schema import OwnerRefs, AssetRefs
from wuttafarm.web.forms.widgets import ImageWidget from wuttafarm.web.forms.widgets import ImageWidget
from wuttafarm.util import get_log_type_enum from wuttafarm.util import get_log_type_enum
from wuttafarm.web.util import get_farmos_client_for_user from wuttafarm.web.util import get_farmos_client_for_user
@ -79,6 +79,7 @@ class AssetMasterView(WuttaFarmMasterView):
form_fields = [ form_fields = [
"asset_name", "asset_name",
"parents",
"notes", "notes",
"asset_type", "asset_type",
"owners", "owners",
@ -279,11 +280,11 @@ class AssetMasterView(WuttaFarmMasterView):
f.set_default("groups", asset_handler.get_groups(asset)) f.set_default("groups", asset_handler.get_groups(asset))
# parents # parents
if self.creating or self.editing: f.set_node("parents", AssetRefs(self.request, for_asset=asset))
f.remove("parents") # TODO: add support for this f.set_required("parents", False)
else: if not self.creating:
f.set_node("parents", AssetParentRefs(self.request)) # nb. must explicity declare value for non-standard field
f.set_default("parents", [p.uuid for p in asset.parents]) f.set_default("parents", asset.parents)
# notes # notes
f.set_widget("notes", "notes") f.set_widget("notes", "notes")
@ -311,11 +312,29 @@ class AssetMasterView(WuttaFarmMasterView):
f.set_default("image", asset.image_url) f.set_default("image", asset.image_url)
def objectify(self, form): def objectify(self, form):
model = self.app.model
session = self.Session()
asset = super().objectify(form) asset = super().objectify(form)
data = form.validated
if self.creating: if self.creating:
asset.asset_type = self.get_asset_type() asset.asset_type = self.get_asset_type()
current = [p.uuid for p in asset.parents]
desired = data["parents"] or []
for uuid in desired:
if uuid not in current:
parent = session.get(model.Asset, uuid)
assert parent
asset.parents.append(parent)
for uuid in current:
if uuid not in desired:
parent = session.get(model.Asset, uuid)
assert parent
asset.parents.remove(parent)
return asset return asset
def get_asset_type(self): def get_asset_type(self):

View file

@ -47,15 +47,11 @@ class GroupView(AssetMasterView):
"archived", "archived",
] ]
form_fields = [ def configure_form(self, f):
"asset_name", super().configure_form(f)
"notes",
"asset_type", # produces_eggs
"produces_eggs", f.fields.insert_after("asset_type", "produces_eggs")
"archived",
"drupal_id",
"farmos_uuid",
]
def defaults(config, **kwargs): def defaults(config, **kwargs):

View file

@ -220,21 +220,6 @@ class PlantAssetView(AssetMasterView):
"archived", "archived",
] ]
form_fields = [
"asset_name",
"plant_types",
"season",
"notes",
"asset_type",
"archived",
"drupal_id",
"farmos_uuid",
"thumbnail_url",
"image_url",
"thumbnail",
"image",
]
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
super().configure_grid(g) super().configure_grid(g)
@ -262,14 +247,15 @@ class PlantAssetView(AssetMasterView):
plant = f.model_instance plant = f.model_instance
# plant_types # plant_types
f.fields.insert_after("asset_name", "plant_types")
f.set_node("plant_types", PlantTypeRefs(self.request)) f.set_node("plant_types", PlantTypeRefs(self.request))
if not self.creating: if not self.creating:
# nb. must explcitly declare value for non-standard field # nb. must explcitly declare value for non-standard field
f.set_default("plant_types", [pt.uuid for pt in plant.plant_types]) f.set_default("plant_types", [pt.uuid for pt in plant.plant_types])
# season # season
if self.creating or self.editing: if not (self.creating or self.editing): # TODO
f.remove("season") # TODO: add support for this f.fields.insert_after("plant_types", "season")
def objectify(self, form): def objectify(self, form):
model = self.app.model model = self.app.model