feat: add schema, edit/sync support for Equipment Assets
This commit is contained in:
parent
03f6da8ab7
commit
42c73375ac
17 changed files with 1182 additions and 48 deletions
|
|
@ -0,0 +1,218 @@
|
|||
"""add EquipmentAsset
|
||||
|
||||
Revision ID: e9b8664e1f39
|
||||
Revises: e5b27eac471c
|
||||
Create Date: 2026-03-09 18:05:54.917562
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e9b8664e1f39"
|
||||
down_revision: Union[str, None] = "e5b27eac471c"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_equipment
|
||||
op.create_table(
|
||||
"asset_equipment",
|
||||
sa.Column("manufacturer", sa.String(length=255), nullable=True),
|
||||
sa.Column("model", sa.String(length=255), nullable=True),
|
||||
sa.Column("serial_number", sa.String(length=255), nullable=True),
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["asset.uuid"], name=op.f("fk_asset_equipment_uuid_asset")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_equipment")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_equipment_version",
|
||||
sa.Column(
|
||||
"manufacturer", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("model", sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"serial_number", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_equipment_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_equipment_version_end_transaction_id"),
|
||||
"asset_equipment_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_equipment_version_operation_type"),
|
||||
"asset_equipment_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_equipment_version_pk_transaction_id",
|
||||
"asset_equipment_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_equipment_version_pk_validity",
|
||||
"asset_equipment_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_equipment_version_transaction_id"),
|
||||
"asset_equipment_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_equipment_equipment_type
|
||||
op.create_table(
|
||||
"asset_equipment_equipment_type",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("equipment_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("equipment_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["equipment_asset_uuid"],
|
||||
["asset_equipment.uuid"],
|
||||
name=op.f(
|
||||
"fk_asset_equipment_equipment_type_equipment_asset_uuid_asset_equipment"
|
||||
),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["equipment_type_uuid"],
|
||||
["equipment_type.uuid"],
|
||||
name=op.f(
|
||||
"fk_asset_equipment_equipment_type_equipment_type_uuid_equipment_type"
|
||||
),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_equipment_equipment_type")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_equipment_equipment_type_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"equipment_asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"equipment_type_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid",
|
||||
"transaction_id",
|
||||
name=op.f("pk_asset_equipment_equipment_type_version"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_equipment_equipment_type_version_end_transaction_id"),
|
||||
"asset_equipment_equipment_type_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_equipment_equipment_type_version_operation_type"),
|
||||
"asset_equipment_equipment_type_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_equipment_equipment_type_version_pk_transaction_id",
|
||||
"asset_equipment_equipment_type_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_equipment_equipment_type_version_pk_validity",
|
||||
"asset_equipment_equipment_type_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_equipment_equipment_type_version_transaction_id"),
|
||||
"asset_equipment_equipment_type_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# asset_equipment_equipment_type
|
||||
op.drop_index(
|
||||
op.f("ix_asset_equipment_equipment_type_version_transaction_id"),
|
||||
table_name="asset_equipment_equipment_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_equipment_equipment_type_version_pk_validity",
|
||||
table_name="asset_equipment_equipment_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_equipment_equipment_type_version_pk_transaction_id",
|
||||
table_name="asset_equipment_equipment_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_equipment_equipment_type_version_operation_type"),
|
||||
table_name="asset_equipment_equipment_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_equipment_equipment_type_version_end_transaction_id"),
|
||||
table_name="asset_equipment_equipment_type_version",
|
||||
)
|
||||
op.drop_table("asset_equipment_equipment_type_version")
|
||||
op.drop_table("asset_equipment_equipment_type")
|
||||
|
||||
# asset_equipment
|
||||
op.drop_index(
|
||||
op.f("ix_asset_equipment_version_transaction_id"),
|
||||
table_name="asset_equipment_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_equipment_version_pk_validity", table_name="asset_equipment_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_equipment_version_pk_transaction_id",
|
||||
table_name="asset_equipment_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_equipment_version_operation_type"),
|
||||
table_name="asset_equipment_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_equipment_version_end_transaction_id"),
|
||||
table_name="asset_equipment_version",
|
||||
)
|
||||
op.drop_table("asset_equipment_version")
|
||||
op.drop_table("asset_equipment")
|
||||
|
|
@ -42,7 +42,7 @@ from .quantities import (
|
|||
from .asset import AssetType, Asset, AssetParent
|
||||
from .asset_land import LandType, LandAsset
|
||||
from .asset_structure import StructureType, StructureAsset
|
||||
from .asset_equipment import EquipmentType
|
||||
from .asset_equipment import EquipmentType, EquipmentAsset, EquipmentAssetEquipmentType
|
||||
from .asset_animal import AnimalType, AnimalAsset
|
||||
from .asset_group import GroupAsset
|
||||
from .asset_plant import (
|
||||
|
|
|
|||
|
|
@ -23,14 +23,19 @@
|
|||
Model definition for Equipment
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.taxonomy import TaxonomyMixin
|
||||
from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies
|
||||
|
||||
|
||||
class EquipmentType(TaxonomyMixin, model.Base):
|
||||
"""
|
||||
Represents a "equipment type" (taxonomy term) from farmOS
|
||||
Represents an "equipment type" (taxonomy term) from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "equipment_type"
|
||||
|
|
@ -39,3 +44,90 @@ class EquipmentType(TaxonomyMixin, model.Base):
|
|||
"model_title": "Equipment Type",
|
||||
"model_title_plural": "Equipment Types",
|
||||
}
|
||||
|
||||
_equipment_assets = orm.relationship(
|
||||
"EquipmentAssetEquipmentType",
|
||||
cascade_backrefs=False,
|
||||
back_populates="equipment_type",
|
||||
)
|
||||
|
||||
|
||||
class EquipmentAsset(AssetMixin, model.Base):
|
||||
"""
|
||||
Represents an equipment asset from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_equipment"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Equipment Asset",
|
||||
"model_title_plural": "Equipment Assets",
|
||||
"farmos_asset_type": "equipment",
|
||||
}
|
||||
|
||||
manufacturer = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Name of the manufacturer, if applicable.
|
||||
""",
|
||||
)
|
||||
|
||||
model = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Model name for the equipment, if applicable.
|
||||
""",
|
||||
)
|
||||
|
||||
serial_number = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Serial number for the equipment, if applicable.
|
||||
""",
|
||||
)
|
||||
|
||||
_equipment_types = orm.relationship(
|
||||
"EquipmentAssetEquipmentType",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="equipment_asset",
|
||||
)
|
||||
|
||||
equipment_types = association_proxy(
|
||||
"_equipment_types",
|
||||
"equipment_type",
|
||||
creator=lambda pt: EquipmentAssetEquipmentType(equipment_type=pt),
|
||||
)
|
||||
|
||||
|
||||
add_asset_proxies(EquipmentAsset)
|
||||
|
||||
|
||||
class EquipmentAssetEquipmentType(model.Base):
|
||||
"""
|
||||
Associates one or more equipment types with an equipment asset.
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_equipment_equipment_type"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
equipment_asset_uuid = model.uuid_fk_column("asset_equipment.uuid", nullable=False)
|
||||
equipment_asset = orm.relationship(
|
||||
EquipmentAsset,
|
||||
foreign_keys=equipment_asset_uuid,
|
||||
back_populates="_equipment_types",
|
||||
)
|
||||
|
||||
equipment_type_uuid = model.uuid_fk_column("equipment_type.uuid", nullable=False)
|
||||
equipment_type = orm.relationship(
|
||||
EquipmentType,
|
||||
doc="""
|
||||
Reference to the equipment type.
|
||||
""",
|
||||
back_populates="_equipment_assets",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -325,6 +325,68 @@ class EquipmentTypeImporter(ToFarmOSTaxonomy):
|
|||
farmos_taxonomy_type = "equipment_type"
|
||||
|
||||
|
||||
class EquipmentAssetImporter(ToFarmOSAsset):
|
||||
|
||||
model_title = "EquipmentAsset"
|
||||
farmos_asset_type = "equipment"
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"manufacturer",
|
||||
"model",
|
||||
"serial_number",
|
||||
"equipment_type_uuids",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_target_object(self, equipment):
|
||||
data = super().normalize_target_object(equipment)
|
||||
data.update(
|
||||
{
|
||||
"manufacturer": equipment["attributes"]["manufacturer"],
|
||||
"model": equipment["attributes"]["model"],
|
||||
"serial_number": equipment["attributes"]["serial_number"],
|
||||
"equipment_type_uuids": [
|
||||
UUID(etype["id"])
|
||||
for etype in equipment["relationships"]["equipment_type"]["data"]
|
||||
],
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def get_asset_payload(self, source_data):
|
||||
payload = super().get_asset_payload(source_data)
|
||||
|
||||
attrs = {}
|
||||
if "manufacturer" in self.fields:
|
||||
attrs["manufacturer"] = source_data["manufacturer"]
|
||||
if "model" in self.fields:
|
||||
attrs["model"] = source_data["model"]
|
||||
if "serial_number" in self.fields:
|
||||
attrs["serial_number"] = source_data["serial_number"]
|
||||
|
||||
rels = {}
|
||||
if "equipment_type_uuids" in self.fields:
|
||||
rels["equipment_type"] = {"data": []}
|
||||
for uuid in source_data["equipment_type_uuids"]:
|
||||
rels["equipment_type"]["data"].append(
|
||||
{
|
||||
"id": str(uuid),
|
||||
"type": "taxonomy_term--equipment_type",
|
||||
}
|
||||
)
|
||||
|
||||
payload["attributes"].update(attrs)
|
||||
if rels:
|
||||
payload.setdefault("relationships", {}).update(rels)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class MaterialTypeImporter(ToFarmOSTaxonomy):
|
||||
|
||||
model_title = "MaterialType"
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
|||
importers["StructureAsset"] = StructureAssetImporter
|
||||
importers["WaterAsset"] = WaterAssetImporter
|
||||
importers["EquipmentType"] = EquipmentTypeImporter
|
||||
importers["EquipmentAsset"] = EquipmentAssetImporter
|
||||
importers["AnimalType"] = AnimalTypeImporter
|
||||
importers["AnimalAsset"] = AnimalAssetImporter
|
||||
importers["GroupAsset"] = GroupAssetImporter
|
||||
|
|
@ -235,6 +236,44 @@ class EquipmentTypeImporter(
|
|||
source_model_class = model.EquipmentType
|
||||
|
||||
|
||||
class EquipmentAssetImporter(
|
||||
FromWuttaFarmAsset, farmos_importing.model.EquipmentAssetImporter
|
||||
):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Equipment Assets
|
||||
"""
|
||||
|
||||
source_model_class = model.EquipmentAsset
|
||||
|
||||
def get_supported_fields(self):
|
||||
fields = list(super().get_supported_fields())
|
||||
|
||||
print(fields)
|
||||
fields.extend(
|
||||
[
|
||||
"manufacturer",
|
||||
"model",
|
||||
"serial_number",
|
||||
"equipment_type_uuids",
|
||||
]
|
||||
)
|
||||
return fields
|
||||
|
||||
def normalize_source_object(self, equipment):
|
||||
data = super().normalize_source_object(equipment)
|
||||
data.update(
|
||||
{
|
||||
"manufacturer": equipment.manufacturer,
|
||||
"model": equipment.model,
|
||||
"serial_number": equipment.serial_number,
|
||||
"equipment_type_uuids": [
|
||||
etype.farmos_uuid for etype in equipment.equipment_types
|
||||
],
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class AnimalTypeImporter(
|
||||
FromWuttaFarmTaxonomy, farmos_importing.model.AnimalTypeImporter
|
||||
):
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
|
|||
importers["StructureAsset"] = StructureAssetImporter
|
||||
importers["WaterAsset"] = WaterAssetImporter
|
||||
importers["EquipmentType"] = EquipmentTypeImporter
|
||||
importers["EquipmentAsset"] = EquipmentAssetImporter
|
||||
importers["AnimalType"] = AnimalTypeImporter
|
||||
importers["AnimalAsset"] = AnimalAssetImporter
|
||||
importers["GroupAsset"] = GroupAssetImporter
|
||||
|
|
@ -489,6 +490,111 @@ class AssetTypeImporter(FromFarmOS, ToWutta):
|
|||
}
|
||||
|
||||
|
||||
class EquipmentAssetImporter(AssetImporterBase):
|
||||
"""
|
||||
farmOS API → WuttaFarm importer for Equipment Assets
|
||||
"""
|
||||
|
||||
model_class = model.EquipmentAsset
|
||||
|
||||
def get_supported_fields(self):
|
||||
fields = list(super().get_supported_fields())
|
||||
fields.extend(
|
||||
[
|
||||
"equipment_types",
|
||||
]
|
||||
)
|
||||
return fields
|
||||
|
||||
def setup(self):
|
||||
super().setup()
|
||||
model = self.app.model
|
||||
|
||||
self.equipment_types_by_farmos_uuid = {}
|
||||
for equipment_type in self.target_session.query(model.EquipmentType):
|
||||
if equipment_type.farmos_uuid:
|
||||
self.equipment_types_by_farmos_uuid[equipment_type.farmos_uuid] = (
|
||||
equipment_type
|
||||
)
|
||||
|
||||
def normalize_source_object(self, equipment):
|
||||
""" """
|
||||
data = super().normalize_source_object(equipment)
|
||||
|
||||
equipment_types = []
|
||||
if relationships := equipment.get("relationships"):
|
||||
|
||||
if equipment_type := relationships.get("equipment_type"):
|
||||
equipment_types = []
|
||||
for equipment_type in equipment_type["data"]:
|
||||
if wf_equipment_type := self.equipment_types_by_farmos_uuid.get(
|
||||
UUID(equipment_type["id"])
|
||||
):
|
||||
equipment_types.append(wf_equipment_type.uuid)
|
||||
else:
|
||||
log.warning(
|
||||
"equipment type not found: %s", equipment_type["id"]
|
||||
)
|
||||
|
||||
data.update(
|
||||
{
|
||||
"manufacturer": equipment["attributes"]["manufacturer"],
|
||||
"model": equipment["attributes"]["model"],
|
||||
"serial_number": equipment["attributes"]["serial_number"],
|
||||
"equipment_types": set(equipment_types),
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def normalize_target_object(self, equipment):
|
||||
data = super().normalize_target_object(equipment)
|
||||
|
||||
if "equipment_types" in self.fields:
|
||||
data["equipment_types"] = set(
|
||||
[etype.uuid for etype in equipment.equipment_types]
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def update_target_object(self, equipment, source_data, target_data=None):
|
||||
model = self.app.model
|
||||
equipment = super().update_target_object(equipment, source_data, target_data)
|
||||
|
||||
if "equipment_types" in self.fields:
|
||||
if (
|
||||
not target_data
|
||||
or target_data["equipment_types"] != source_data["equipment_types"]
|
||||
):
|
||||
|
||||
for uuid in source_data["equipment_types"]:
|
||||
if not target_data or uuid not in target_data["equipment_types"]:
|
||||
self.target_session.flush()
|
||||
equipment._equipment_types.append(
|
||||
model.EquipmentAssetEquipmentType(equipment_type_uuid=uuid)
|
||||
)
|
||||
|
||||
if target_data:
|
||||
for uuid in target_data["equipment_types"]:
|
||||
if uuid not in source_data["equipment_types"]:
|
||||
equipment_type = (
|
||||
self.target_session.query(
|
||||
model.EquipmentAssetEquipmentType
|
||||
)
|
||||
.filter(
|
||||
model.EquipmentAssetEquipmentType.equipment_asset
|
||||
== equipment
|
||||
)
|
||||
.filter(
|
||||
model.EquipmentAssetEquipmentType.equipment_type_uuid
|
||||
== uuid
|
||||
)
|
||||
.one()
|
||||
)
|
||||
self.target_session.delete(equipment_type)
|
||||
|
||||
return equipment
|
||||
|
||||
|
||||
class GroupAssetImporter(AssetImporterBase):
|
||||
"""
|
||||
farmOS API → WuttaFarm importer for Group Assets
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import json
|
|||
import colander
|
||||
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.forms.schema import ObjectRef, WuttaSet
|
||||
from wuttaweb.forms.schema import ObjectRef, WuttaSet, WuttaList
|
||||
from wuttaweb.forms.widgets import NotesWidget
|
||||
|
||||
|
||||
|
|
@ -216,6 +216,35 @@ class FarmOSQuantityRefs(WuttaSet):
|
|||
return FarmOSQuantityRefsWidget(**kwargs)
|
||||
|
||||
|
||||
class FarmOSTaxonomyTerms(colander.SchemaType):
|
||||
"""
|
||||
Schema type which can represent multiple taxonomy terms.
|
||||
"""
|
||||
|
||||
route_prefix = None
|
||||
|
||||
def __init__(self, request, route_prefix=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
if route_prefix:
|
||||
self.route_prefix = route_prefix
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
return appstruct
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import FarmOSTaxonomyTermsWidget
|
||||
|
||||
return FarmOSTaxonomyTermsWidget(self.request, self.route_prefix, **kwargs)
|
||||
|
||||
|
||||
class FarmOSEquipmentTypeRefs(FarmOSTaxonomyTerms):
|
||||
|
||||
route_prefix = "farmos_equipment_types"
|
||||
|
||||
|
||||
class FarmOSPlantTypes(colander.SchemaType):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
|
|
@ -260,6 +289,35 @@ class LandTypeRef(ObjectRef):
|
|||
return self.request.route_url("land_types.view", uuid=land_type.uuid)
|
||||
|
||||
|
||||
class TaxonomyTermRefs(WuttaList):
|
||||
"""
|
||||
Generic schema type for a field which can reference multiple
|
||||
taxonomy terms.
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
terms = []
|
||||
for term in appstruct:
|
||||
terms.append(
|
||||
{
|
||||
"uuid": str(term.uuid),
|
||||
"name": term.name,
|
||||
}
|
||||
)
|
||||
return terms
|
||||
|
||||
|
||||
class EquipmentTypeRefs(TaxonomyTermRefs):
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import EquipmentTypeRefsWidget
|
||||
|
||||
return EquipmentTypeRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class PlantTypeRefs(WuttaSet):
|
||||
"""
|
||||
Schema type for Plant Types field (on a Plant Asset).
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget
|
|||
from wuttaweb.db import Session
|
||||
|
||||
from wuttafarm.web.util import render_quantity_objects
|
||||
from wuttafarm.db.model import EquipmentType
|
||||
|
||||
|
||||
class ImageWidget(Widget):
|
||||
|
|
@ -228,6 +229,38 @@ class FarmOSUnitRefWidget(Widget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSTaxonomyTermsWidget(Widget):
|
||||
"""
|
||||
Widget to display a field which can reference multiple taxonomy
|
||||
terms.
|
||||
"""
|
||||
|
||||
def __init__(self, request, route_prefix, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.route_prefix = route_prefix
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
if cstruct in (colander.null, None):
|
||||
return HTML.tag("span")
|
||||
|
||||
links = []
|
||||
for term in cstruct:
|
||||
link = tags.link_to(
|
||||
term["name"],
|
||||
self.request.route_url(
|
||||
f"{self.route_prefix}.view", uuid=term["uuid"]
|
||||
),
|
||||
)
|
||||
links.append(HTML.tag("li", c=link))
|
||||
return HTML.tag("ul", c=links)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSPlantTypesWidget(Widget):
|
||||
"""
|
||||
Widget to display a farmOS "plant types" field.
|
||||
|
|
@ -258,6 +291,88 @@ class FarmOSPlantTypesWidget(Widget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class TaxonomyTermRefsWidget(Widget):
|
||||
"""
|
||||
Generic (incomplete) widget for fields which can reference
|
||||
multiple taxonomy terms.
|
||||
|
||||
This widget can handle typical read-only scenarios but the
|
||||
editable mode is not implemented.
|
||||
"""
|
||||
|
||||
route_prefix = None
|
||||
|
||||
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()
|
||||
|
||||
@classmethod
|
||||
def get_route_prefix(cls):
|
||||
return cls.route_prefix
|
||||
|
||||
@classmethod
|
||||
def get_permission_prefix(cls):
|
||||
return cls.route_prefix
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
if not cstruct:
|
||||
cstruct = []
|
||||
|
||||
if readonly := kw.get("readonly", self.readonly):
|
||||
items = []
|
||||
route_prefix = self.get_route_prefix()
|
||||
for term in cstruct:
|
||||
url = self.request.route_url(f"{route_prefix}.view", uuid=term["uuid"])
|
||||
link = tags.link_to(term["name"], url)
|
||||
items.append(HTML.tag("li", c=link))
|
||||
return HTML.tag("ul", c=items)
|
||||
|
||||
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)
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
terms = []
|
||||
query = session.query(self.model_class).order_by(self.model_class.name)
|
||||
for term in query:
|
||||
terms.append(
|
||||
{
|
||||
"uuid": str(term.uuid),
|
||||
"name": term.name,
|
||||
}
|
||||
)
|
||||
values["terms"] = terms
|
||||
|
||||
permission_prefix = self.get_permission_prefix()
|
||||
if self.request.has_perm(f"{permission_prefix}.create"):
|
||||
values["can_create"] = True
|
||||
|
||||
return values
|
||||
|
||||
def deserialize(self, field, pstruct):
|
||||
""" """
|
||||
if not pstruct:
|
||||
return colander.null
|
||||
|
||||
return json.loads(pstruct)
|
||||
|
||||
|
||||
class EquipmentTypeRefsWidget(TaxonomyTermRefsWidget):
|
||||
"""
|
||||
Widget for Equipment Types field.
|
||||
"""
|
||||
|
||||
model_class = EquipmentType
|
||||
route_prefix = "equipment_types"
|
||||
template = "equipmenttyperefs"
|
||||
|
||||
|
||||
class PlantTypeRefsWidget(Widget):
|
||||
"""
|
||||
Widget for Plant Types field (on a Plant Asset).
|
||||
|
|
|
|||
|
|
@ -92,6 +92,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"route": "animal_assets",
|
||||
"perm": "animal_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Equipment",
|
||||
"route": "equipment_assets",
|
||||
"perm": "equipment_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Group",
|
||||
"route": "group_assets",
|
||||
|
|
@ -249,6 +254,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"route": "farmos_animal_assets",
|
||||
"perm": "farmos_animal_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Equipment Assets",
|
||||
"route": "farmos_equipment_assets",
|
||||
"perm": "farmos_equipment_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Group Assets",
|
||||
"route": "farmos_group_assets",
|
||||
|
|
@ -383,6 +393,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"route": "farmos_animal_assets",
|
||||
"perm": "farmos_animal_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Equipment",
|
||||
"route": "farmos_equipment_assets",
|
||||
"perm": "farmos_equipment_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Group",
|
||||
"route": "farmos_group_assets",
|
||||
|
|
|
|||
13
src/wuttafarm/web/templates/deform/equipmenttyperefs.pt
Normal file
13
src/wuttafarm/web/templates/deform/equipmenttyperefs.pt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div tal:define="
|
||||
name name|field.name;
|
||||
oid oid|field.oid;
|
||||
vmodel vmodel|'modelData.'+oid;
|
||||
can_create can_create|False;"
|
||||
tal:omit-tag="">
|
||||
|
||||
<equipment-types-picker tal:attributes="name name;
|
||||
v-model vmodel;
|
||||
:equipment-types terms;
|
||||
:can-create str(can_create).lower();" />
|
||||
|
||||
</div>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
|
||||
<%def name="make_wuttafarm_components()">
|
||||
${self.make_taxonomy_terms_picker_component()}
|
||||
${self.make_equipment_types_picker_component()}
|
||||
${self.make_assets_picker_component()}
|
||||
${self.make_animal_type_picker_component()}
|
||||
${self.make_material_types_picker_component()}
|
||||
|
|
@ -9,6 +11,216 @@
|
|||
${self.make_seasons_picker_component()}
|
||||
</%def>
|
||||
|
||||
<%def name="make_taxonomy_terms_picker_component()">
|
||||
<script type="text/x-template" id="taxonomy-terms-picker-template">
|
||||
<div>
|
||||
<input type="hidden" :name="name" :value="JSON.stringify(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>
|
||||
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="createInit()">
|
||||
New
|
||||
</b-button>
|
||||
|
||||
</div>
|
||||
|
||||
<${b}-table :data="value">
|
||||
|
||||
<${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="removeTerm(props.row)">
|
||||
<i class="fas fa-trash" /> Remove
|
||||
</a>
|
||||
</${b}-table-column>
|
||||
|
||||
</${b}-table>
|
||||
|
||||
<${b}-modal v-if="canCreate"
|
||||
has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="createShowDialog"
|
||||
% else:
|
||||
:active.sync="createShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">New {{ termTitle }} </p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<b-field label="Name" horizontal>
|
||||
<b-input v-model="createName"
|
||||
ref="createName"
|
||||
expanded
|
||||
@keydown.native="createNameKeydown" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="createSave()"
|
||||
:disabled="createSaving || !createName"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ createSaving ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="createShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
const TaxonomyTermsPicker = {
|
||||
template: '#taxonomy-terms-picker-template',
|
||||
mixins: [WuttaRequestMixin],
|
||||
props: {
|
||||
name: String,
|
||||
value: Array,
|
||||
termTitle: String,
|
||||
terms: Array,
|
||||
canCreate: Boolean,
|
||||
createUrl: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addName: '',
|
||||
createShowDialog: false,
|
||||
createName: null,
|
||||
createSaving: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
addNameData() {
|
||||
if (!this.addName) {
|
||||
return this.terms
|
||||
}
|
||||
|
||||
return this.terms.filter((term) => {
|
||||
return term.name.toLowerCase().indexOf(this.addName.toLowerCase()) >= 0
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
addNameSelected(option) {
|
||||
if (!option) {
|
||||
return
|
||||
}
|
||||
|
||||
const uuids = this.value.map((term) => {
|
||||
return term.uuid
|
||||
})
|
||||
|
||||
if (!uuids.includes(option.uuid)) {
|
||||
this.value.push(option)
|
||||
this.$emit('input', this.value)
|
||||
}
|
||||
|
||||
this.addName = null
|
||||
},
|
||||
|
||||
createInit() {
|
||||
this.createName = this.addName
|
||||
this.createShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.createName.focus()
|
||||
})
|
||||
},
|
||||
|
||||
createNameKeydown(event) {
|
||||
// nb. must prevent main form submit on ENTER
|
||||
// (since ultimately this lives within an outer form)
|
||||
// but also we can submit the modal pseudo-form
|
||||
if (event.which == 13) {
|
||||
event.preventDefault()
|
||||
this.createSave()
|
||||
}
|
||||
},
|
||||
|
||||
createSave() {
|
||||
this.createSaving = true
|
||||
const params = {name: this.createName}
|
||||
this.wuttaPOST(this.createUrl, params, response => {
|
||||
this.value.push(response.data)
|
||||
this.$emit('input', this.value)
|
||||
this.addName = null
|
||||
this.createSaving = false
|
||||
this.createShowDialog = false
|
||||
}, response => {
|
||||
this.createSaving = false
|
||||
})
|
||||
},
|
||||
|
||||
removeTerm(term) {
|
||||
const value = Array.from(this.value)
|
||||
const i = value.indexOf(term)
|
||||
value.splice(i, 1)
|
||||
this.$emit('input', value)
|
||||
},
|
||||
},
|
||||
}
|
||||
Vue.component('taxonomy-terms-picker', TaxonomyTermsPicker)
|
||||
<% request.register_component('taxonomy-terms-picker', 'TaxonomyTermsPicker') %>
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="make_equipment_types_picker_component()">
|
||||
<script type="text/x-template" id="equipment-types-picker-template">
|
||||
<taxonomy-terms-picker v-model="proxyValue"
|
||||
:name="name"
|
||||
term-title="Equipment Type"
|
||||
:terms="equipmentTypes"
|
||||
:can-create="canCreate"
|
||||
create-url="${url('equipment_types.ajax_create')}" />
|
||||
</script>
|
||||
<script>
|
||||
const EquipmentTypesPicker = {
|
||||
template: '#equipment-types-picker-template',
|
||||
props: {
|
||||
name: String,
|
||||
value: Array,
|
||||
equipmentTypes: Array,
|
||||
canCreate: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
proxyValue: this.value || [],
|
||||
}
|
||||
},
|
||||
}
|
||||
Vue.component('equipment-types-picker', EquipmentTypesPicker)
|
||||
<% request.register_component('equipment-types-picker', 'EquipmentTypesPicker') %>
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="make_assets_picker_component()">
|
||||
<script type="text/x-template" id="assets-picker-template">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -83,9 +83,9 @@ class AssetMasterView(WuttaFarmMasterView):
|
|||
"notes",
|
||||
"asset_type",
|
||||
"owners",
|
||||
"locations",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"locations",
|
||||
"groups",
|
||||
"archived",
|
||||
"drupal_id",
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@
|
|||
Master view for Plants
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model import EquipmentType
|
||||
from wuttafarm.db.model import EquipmentType, EquipmentAsset
|
||||
from wuttafarm.web.views import TaxonomyMasterView
|
||||
from wuttafarm.web.views.assets import AssetMasterView
|
||||
from wuttafarm.web.forms.schema import EquipmentTypeRefs
|
||||
|
||||
|
||||
class EquipmentTypeView(TaxonomyMasterView):
|
||||
|
|
@ -42,12 +44,79 @@ class EquipmentTypeView(TaxonomyMasterView):
|
|||
farmos_refurl_path = "/admin/structure/taxonomy/manage/equipment_type/overview"
|
||||
|
||||
|
||||
class EquipmentAssetView(AssetMasterView):
|
||||
"""
|
||||
Master view for Equipment Assets
|
||||
"""
|
||||
|
||||
model_class = EquipmentAsset
|
||||
route_prefix = "equipment_assets"
|
||||
url_prefix = "/assets/equipment"
|
||||
|
||||
farmos_bundle = "equipment"
|
||||
farmos_refurl_path = "/assets/equipment"
|
||||
|
||||
labels = {
|
||||
"equipment_types": "Equipment Type",
|
||||
}
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
equipment = f.model_instance
|
||||
|
||||
# equipment_types
|
||||
f.fields.insert_after("asset_name", "equipment_types")
|
||||
f.set_node("equipment_types", EquipmentTypeRefs(self.request))
|
||||
if not self.creating:
|
||||
# nb. must explcitly declare value for non-standard field
|
||||
f.set_default("equipment_types", equipment.equipment_types)
|
||||
|
||||
# manufacturer
|
||||
f.fields.insert_after("equipment_types", "manufacturer")
|
||||
|
||||
# model
|
||||
f.fields.insert_after("manufacturer", "model")
|
||||
|
||||
# serial_number
|
||||
f.fields.insert_after("model", "serial_number")
|
||||
|
||||
def objectify(self, form):
|
||||
equipment = super().objectify(form)
|
||||
data = form.validated
|
||||
|
||||
self.set_equipment_types(equipment, data["equipment_types"])
|
||||
|
||||
return equipment
|
||||
|
||||
def set_equipment_types(self, equipment, desired):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
current = [str(etype.uuid) for etype in equipment.equipment_types]
|
||||
|
||||
for etype in desired:
|
||||
if etype["uuid"] not in current:
|
||||
equipment_type = session.get(model.EquipmentType, etype["uuid"])
|
||||
assert equipment_type
|
||||
equipment.equipment_types.append(equipment_type)
|
||||
|
||||
desired = [etype["uuid"] for etype in desired]
|
||||
for uuid in current:
|
||||
if uuid not in desired:
|
||||
equipment_type = session.get(model.EquipmentType, uuid)
|
||||
assert equipment_type
|
||||
equipment.equipment_types.remove(equipment_type)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
EquipmentTypeView = kwargs.get("EquipmentTypeView", base["EquipmentTypeView"])
|
||||
EquipmentTypeView.defaults(config)
|
||||
|
||||
EquipmentAssetView = kwargs.get("EquipmentAssetView", base["EquipmentAssetView"])
|
||||
EquipmentAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from wuttafarm.web.grids import (
|
|||
NullableBooleanFilter,
|
||||
DateTimeFilter,
|
||||
)
|
||||
from wuttafarm.web.forms.schema import FarmOSRef, FarmOSAssetRefs
|
||||
from wuttafarm.web.forms.schema import FarmOSRef
|
||||
|
||||
|
||||
class AnimalView(AssetMasterView):
|
||||
|
|
@ -99,8 +99,7 @@ class AnimalView(AssetMasterView):
|
|||
|
||||
def get_farmos_api_includes(self):
|
||||
includes = super().get_farmos_api_includes()
|
||||
includes.add("animal_type")
|
||||
includes.add("group")
|
||||
includes.update(["animal_type"])
|
||||
return includes
|
||||
|
||||
def configure_grid(self, grid):
|
||||
|
|
@ -131,10 +130,6 @@ class AnimalView(AssetMasterView):
|
|||
g.set_sorter("sex", SimpleSorter("sex"))
|
||||
g.set_filter("sex", StringFilter)
|
||||
|
||||
# groups
|
||||
g.set_label("groups", "Group Membership")
|
||||
g.set_renderer("groups", self.render_groups_for_grid)
|
||||
|
||||
# is_sterile
|
||||
g.set_renderer("is_sterile", "boolean")
|
||||
g.set_sorter("is_sterile", SimpleSorter("is_sterile"))
|
||||
|
|
@ -145,18 +140,6 @@ class AnimalView(AssetMasterView):
|
|||
url = self.request.route_url("farmos_animal_types.view", uuid=uuid)
|
||||
return tags.link_to(value, url)
|
||||
|
||||
def render_groups_for_grid(self, animal, field, value):
|
||||
groups = []
|
||||
for group in animal["groups"]:
|
||||
if self.farmos_style_grid_links:
|
||||
url = self.request.route_url(
|
||||
"farmos_group_assets.view", uuid=group["uuid"]
|
||||
)
|
||||
groups.append(tags.link_to(group["name"], url))
|
||||
else:
|
||||
groups.append(group["name"])
|
||||
return ", ".join(groups)
|
||||
|
||||
def get_instance(self):
|
||||
|
||||
data = super().get_instance()
|
||||
|
|
@ -192,8 +175,6 @@ class AnimalView(AssetMasterView):
|
|||
sterile = animal["attributes"]["is_castrated"]
|
||||
|
||||
animal_type_object = None
|
||||
group_objects = []
|
||||
group_names = []
|
||||
if relationships := animal.get("relationships"):
|
||||
|
||||
if animal_type := relationships.get("animal_type"):
|
||||
|
|
@ -203,24 +184,11 @@ class AnimalView(AssetMasterView):
|
|||
"name": animal_type["attributes"]["name"],
|
||||
}
|
||||
|
||||
if groups := relationships.get("group"):
|
||||
for group in groups["data"]:
|
||||
if group := included.get(group["id"]):
|
||||
group = {
|
||||
"uuid": group["id"],
|
||||
"name": group["attributes"]["name"],
|
||||
"asset_type": "group",
|
||||
}
|
||||
group_objects.append(group)
|
||||
group_names.append(group["name"])
|
||||
|
||||
normal.update(
|
||||
{
|
||||
"animal_type": animal_type_object,
|
||||
"animal_type_uuid": animal_type_object["uuid"],
|
||||
"animal_type_name": animal_type_object["name"],
|
||||
"groups": group_objects,
|
||||
"group_names": group_names,
|
||||
"birthdate": birthdate,
|
||||
"sex": animal["attributes"]["sex"] or colander.null,
|
||||
"is_sterile": sterile,
|
||||
|
|
@ -271,12 +239,6 @@ class AnimalView(AssetMasterView):
|
|||
# is_sterile
|
||||
f.set_node("is_sterile", colander.Boolean())
|
||||
|
||||
# groups
|
||||
if self.creating or self.editing:
|
||||
f.remove("groups") # TODO
|
||||
else:
|
||||
f.set_node("groups", FarmOSAssetRefs(self.request))
|
||||
|
||||
def get_api_payload(self, animal):
|
||||
payload = super().get_api_payload(animal)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import requests
|
|||
from webhelpers2.html import tags
|
||||
|
||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||
from wuttafarm.web.forms.schema import FarmOSRefs, FarmOSLocationRefs
|
||||
from wuttafarm.web.forms.schema import FarmOSRefs, FarmOSAssetRefs, FarmOSLocationRefs
|
||||
from wuttafarm.web.forms.widgets import ImageWidget
|
||||
from wuttafarm.web.grids import (
|
||||
ResourceData,
|
||||
|
|
@ -80,12 +80,13 @@ class AssetMasterView(FarmOSMasterView):
|
|||
"name",
|
||||
"notes",
|
||||
"asset_type_name",
|
||||
"owners",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"owners",
|
||||
"locations",
|
||||
"groups",
|
||||
"archived",
|
||||
"drupal_id",
|
||||
"thumbnail_url",
|
||||
"image_url",
|
||||
"thumbnail",
|
||||
|
|
@ -127,6 +128,10 @@ class AssetMasterView(FarmOSMasterView):
|
|||
# locations
|
||||
g.set_renderer("locations", self.render_locations_for_grid)
|
||||
|
||||
# groups
|
||||
g.set_label("groups", "Group Membership")
|
||||
g.set_renderer("groups", self.render_assets_for_grid)
|
||||
|
||||
# archived
|
||||
g.set_renderer("archived", "boolean")
|
||||
g.set_sorter("archived", SimpleSorter("archived"))
|
||||
|
|
@ -137,6 +142,20 @@ class AssetMasterView(FarmOSMasterView):
|
|||
return tags.image(url, f"thumbnail for {self.get_model_title()}")
|
||||
return None
|
||||
|
||||
def render_assets_for_grid(self, log, field, value):
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
assets = []
|
||||
for asset in value:
|
||||
if self.farmos_style_grid_links:
|
||||
route = f"farmos_{asset['asset_type']}_assets.view"
|
||||
url = self.request.route_url(route, uuid=asset["uuid"])
|
||||
assets.append(tags.link_to(asset["name"], url))
|
||||
else:
|
||||
assets.append(asset["name"])
|
||||
return ", ".join(assets)
|
||||
|
||||
def render_locations_for_grid(self, asset, field, value):
|
||||
locations = []
|
||||
for location in value:
|
||||
|
|
@ -156,7 +175,7 @@ class AssetMasterView(FarmOSMasterView):
|
|||
return None
|
||||
|
||||
def get_farmos_api_includes(self):
|
||||
return {"asset_type", "location", "owner", "image"}
|
||||
return {"asset_type", "location", "group", "owner", "image"}
|
||||
|
||||
def get_instance(self):
|
||||
try:
|
||||
|
|
@ -192,6 +211,7 @@ class AssetMasterView(FarmOSMasterView):
|
|||
owner_names = []
|
||||
location_objects = []
|
||||
location_names = []
|
||||
group_objects = []
|
||||
thumbnail_url = None
|
||||
image_url = None
|
||||
if relationships := asset.get("relationships"):
|
||||
|
|
@ -225,6 +245,16 @@ class AssetMasterView(FarmOSMasterView):
|
|||
location_objects.append(location)
|
||||
location_names.append(location["name"])
|
||||
|
||||
if groups := relationships.get("group"):
|
||||
for group in groups["data"]:
|
||||
if group := included.get(group["id"]):
|
||||
group = {
|
||||
"uuid": group["id"],
|
||||
"name": group["attributes"]["name"],
|
||||
"asset_type": "group",
|
||||
}
|
||||
group_objects.append(group)
|
||||
|
||||
if images := relationships.get("image"):
|
||||
for image in images["data"]:
|
||||
if image := included.get(image["id"]):
|
||||
|
|
@ -246,6 +276,7 @@ class AssetMasterView(FarmOSMasterView):
|
|||
"owner_names": owner_names,
|
||||
"locations": location_objects,
|
||||
"location_names": location_names,
|
||||
"groups": group_objects,
|
||||
"archived": archived,
|
||||
"thumbnail_url": thumbnail_url or colander.null,
|
||||
"image_url": image_url or colander.null,
|
||||
|
|
@ -267,6 +298,12 @@ class AssetMasterView(FarmOSMasterView):
|
|||
f.set_label("locations", "Current Location")
|
||||
f.set_node("locations", FarmOSLocationRefs(self.request))
|
||||
|
||||
# groups
|
||||
if self.creating or self.editing:
|
||||
f.remove("groups") # TODO
|
||||
else:
|
||||
f.set_node("groups", FarmOSAssetRefs(self.request))
|
||||
|
||||
# owners
|
||||
if self.creating or self.editing:
|
||||
f.remove("owners") # TODO
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@
|
|||
Master view for farmOS Equipment
|
||||
"""
|
||||
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
||||
from wuttafarm.web.views.farmos.assets import AssetMasterView
|
||||
from wuttafarm.web.forms.schema import FarmOSEquipmentTypeRefs
|
||||
|
||||
|
||||
class EquipmentTypeView(TaxonomyMasterView):
|
||||
|
|
@ -65,12 +69,143 @@ class EquipmentTypeView(TaxonomyMasterView):
|
|||
return buttons
|
||||
|
||||
|
||||
class EquipmentAssetView(AssetMasterView):
|
||||
"""
|
||||
Master view for farmOS Equipment Assets
|
||||
"""
|
||||
|
||||
model_name = "farmos_equipment_assets"
|
||||
model_title = "farmOS Equipment Asset"
|
||||
model_title_plural = "farmOS Equipment Assets"
|
||||
|
||||
route_prefix = "farmos_equipment_assets"
|
||||
url_prefix = "/farmOS/assets/equipment"
|
||||
|
||||
farmos_asset_type = "equipment"
|
||||
farmos_refurl_path = "/assets/equipment"
|
||||
|
||||
labels = {
|
||||
"equipment_types": "Equipment Type",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"thumbnail",
|
||||
"drupal_id",
|
||||
"name",
|
||||
"equipment_types",
|
||||
"manufacturer",
|
||||
"model",
|
||||
"serial_number",
|
||||
"groups",
|
||||
"owners",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def get_farmos_api_includes(self):
|
||||
includes = super().get_farmos_api_includes()
|
||||
includes.update(["equipment_type"])
|
||||
return includes
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# equipment_types
|
||||
g.set_renderer("equipment_types", self.render_equipment_types_for_grid)
|
||||
|
||||
def render_equipment_types_for_grid(self, equipment, field, value):
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for etype in value:
|
||||
url = self.request.route_url(
|
||||
f"farmos_equipment_types.view", uuid=etype["uuid"]
|
||||
)
|
||||
links.append(tags.link_to(etype["name"], url))
|
||||
return ", ".join(links)
|
||||
|
||||
return ", ".join([etype["name"] for etype in value])
|
||||
|
||||
def normalize_asset(self, equipment, included):
|
||||
data = super().normalize_asset(equipment, included)
|
||||
|
||||
equipment_type_objects = []
|
||||
rels = equipment["relationships"]
|
||||
for etype in rels["equipment_type"]["data"]:
|
||||
uuid = etype["id"]
|
||||
equipment_type = {
|
||||
"uuid": uuid,
|
||||
"type": etype["type"],
|
||||
}
|
||||
if etype := included.get(uuid):
|
||||
equipment_type.update(
|
||||
{
|
||||
"name": etype["attributes"]["name"],
|
||||
}
|
||||
)
|
||||
equipment_type_objects.append(equipment_type)
|
||||
|
||||
data.update(
|
||||
{
|
||||
"manufacturer": equipment["attributes"]["manufacturer"],
|
||||
"model": equipment["attributes"]["model"],
|
||||
"serial_number": equipment["attributes"]["serial_number"],
|
||||
"equipment_types": equipment_type_objects,
|
||||
}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
||||
# equipment_types
|
||||
f.fields.insert_after("name", "equipment_types")
|
||||
f.set_node("equipment_types", FarmOSEquipmentTypeRefs(self.request))
|
||||
|
||||
# manufacturer
|
||||
f.fields.insert_after("equipment_types", "manufacturer")
|
||||
|
||||
# model
|
||||
f.fields.insert_after("manufacturer", "model")
|
||||
|
||||
# serial_number
|
||||
f.fields.insert_after("model", "serial_number")
|
||||
|
||||
def get_xref_buttons(self, equipment):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
buttons = super().get_xref_buttons(equipment)
|
||||
|
||||
if wf_equipment := (
|
||||
session.query(model.Asset)
|
||||
.filter(model.Asset.farmos_uuid == equipment["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"equipment_assets.view", uuid=wf_equipment.uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
EquipmentTypeView = kwargs.get("EquipmentTypeView", base["EquipmentTypeView"])
|
||||
EquipmentTypeView.defaults(config)
|
||||
|
||||
EquipmentAssetView = kwargs.get("EquipmentAssetView", base["EquipmentAssetView"])
|
||||
EquipmentAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ Base class for WuttaFarm master views
|
|||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.util import get_form_data
|
||||
|
||||
from wuttafarm.web.util import use_farmos_style_grid_links, get_farmos_client_for_user
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue