feat: add edit/sync support for Plant Seasons
This commit is contained in:
parent
e61043b9d9
commit
45fd5556f2
15 changed files with 1029 additions and 27 deletions
|
|
@ -0,0 +1,205 @@
|
||||||
|
"""add plant seasons
|
||||||
|
|
||||||
|
Revision ID: c5183b781d34
|
||||||
|
Revises: 5f474125a80e
|
||||||
|
Create Date: 2026-03-06 20:18:40.160531
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wuttjamaican.db.util
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "c5183b781d34"
|
||||||
|
down_revision: Union[str, None] = "5f474125a80e"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
|
||||||
|
# season
|
||||||
|
op.create_table(
|
||||||
|
"season",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=100), nullable=False),
|
||||||
|
sa.Column("description", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
|
sa.Column("drupal_id", sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_season")),
|
||||||
|
sa.UniqueConstraint("drupal_id", name=op.f("uq_season_drupal_id")),
|
||||||
|
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_season_farmos_uuid")),
|
||||||
|
sa.UniqueConstraint("name", name=op.f("uq_season_name")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"season_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"description", sa.String(length=255), autoincrement=False, nullable=True
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"farmos_uuid",
|
||||||
|
wuttjamaican.db.util.UUID(),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.Column("drupal_id", sa.Integer(), 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_season_version")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_season_version_end_transaction_id"),
|
||||||
|
"season_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_season_version_operation_type"),
|
||||||
|
"season_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_season_version_pk_transaction_id",
|
||||||
|
"season_version",
|
||||||
|
["uuid", sa.literal_column("transaction_id DESC")],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_season_version_pk_validity",
|
||||||
|
"season_version",
|
||||||
|
["uuid", "transaction_id", "end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_season_version_transaction_id"),
|
||||||
|
"season_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# asset_plant_season
|
||||||
|
op.create_table(
|
||||||
|
"asset_plant_season",
|
||||||
|
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("plant_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.Column("season_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["plant_asset_uuid"],
|
||||||
|
["asset_plant.uuid"],
|
||||||
|
name=op.f("fk_asset_plant_season_plant_asset_uuid_asset_plant"),
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["season_uuid"],
|
||||||
|
["season.uuid"],
|
||||||
|
name=op.f("fk_asset_plant_season_season_uuid_season"),
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_plant_season")),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"asset_plant_season_version",
|
||||||
|
sa.Column(
|
||||||
|
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"plant_asset_uuid",
|
||||||
|
wuttjamaican.db.util.UUID(),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"season_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_plant_season_version")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_asset_plant_season_version_end_transaction_id"),
|
||||||
|
"asset_plant_season_version",
|
||||||
|
["end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_asset_plant_season_version_operation_type"),
|
||||||
|
"asset_plant_season_version",
|
||||||
|
["operation_type"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_asset_plant_season_version_pk_transaction_id",
|
||||||
|
"asset_plant_season_version",
|
||||||
|
["uuid", sa.literal_column("transaction_id DESC")],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_asset_plant_season_version_pk_validity",
|
||||||
|
"asset_plant_season_version",
|
||||||
|
["uuid", "transaction_id", "end_transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_asset_plant_season_version_transaction_id"),
|
||||||
|
"asset_plant_season_version",
|
||||||
|
["transaction_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
|
||||||
|
# asset_plant_season
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_asset_plant_season_version_transaction_id"),
|
||||||
|
table_name="asset_plant_season_version",
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
"ix_asset_plant_season_version_pk_validity",
|
||||||
|
table_name="asset_plant_season_version",
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
"ix_asset_plant_season_version_pk_transaction_id",
|
||||||
|
table_name="asset_plant_season_version",
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_asset_plant_season_version_operation_type"),
|
||||||
|
table_name="asset_plant_season_version",
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_asset_plant_season_version_end_transaction_id"),
|
||||||
|
table_name="asset_plant_season_version",
|
||||||
|
)
|
||||||
|
op.drop_table("asset_plant_season_version")
|
||||||
|
op.drop_table("asset_plant_season")
|
||||||
|
|
||||||
|
# season
|
||||||
|
op.drop_index(op.f("ix_season_version_transaction_id"), table_name="season_version")
|
||||||
|
op.drop_index("ix_season_version_pk_validity", table_name="season_version")
|
||||||
|
op.drop_index("ix_season_version_pk_transaction_id", table_name="season_version")
|
||||||
|
op.drop_index(op.f("ix_season_version_operation_type"), table_name="season_version")
|
||||||
|
op.drop_index(
|
||||||
|
op.f("ix_season_version_end_transaction_id"), table_name="season_version"
|
||||||
|
)
|
||||||
|
op.drop_table("season_version")
|
||||||
|
op.drop_table("season")
|
||||||
|
|
@ -37,7 +37,13 @@ from .asset_land import LandType, LandAsset
|
||||||
from .asset_structure import StructureType, StructureAsset
|
from .asset_structure import StructureType, StructureAsset
|
||||||
from .asset_animal import AnimalType, AnimalAsset
|
from .asset_animal import AnimalType, AnimalAsset
|
||||||
from .asset_group import GroupAsset
|
from .asset_group import GroupAsset
|
||||||
from .asset_plant import PlantType, PlantAsset, PlantAssetPlantType
|
from .asset_plant import (
|
||||||
|
PlantType,
|
||||||
|
Season,
|
||||||
|
PlantAsset,
|
||||||
|
PlantAssetPlantType,
|
||||||
|
PlantAssetSeason,
|
||||||
|
)
|
||||||
from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner
|
from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner
|
||||||
from .log_activity import ActivityLog
|
from .log_activity import ActivityLog
|
||||||
from .log_harvest import HarvestLog
|
from .log_harvest import HarvestLog
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,65 @@ class PlantType(model.Base):
|
||||||
return self.name or ""
|
return self.name or ""
|
||||||
|
|
||||||
|
|
||||||
|
class Season(model.Base):
|
||||||
|
"""
|
||||||
|
Represents a "season" (taxonomy term) from farmOS
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "season"
|
||||||
|
__versioned__ = {}
|
||||||
|
__wutta_hint__ = {
|
||||||
|
"model_title": "Season",
|
||||||
|
"model_title_plural": "Seasons",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
name = sa.Column(
|
||||||
|
sa.String(length=100),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
doc="""
|
||||||
|
Name of the season.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
description = sa.Column(
|
||||||
|
sa.String(length=255),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Optional description for the season.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
farmos_uuid = sa.Column(
|
||||||
|
model.UUID(),
|
||||||
|
nullable=True,
|
||||||
|
unique=True,
|
||||||
|
doc="""
|
||||||
|
UUID for the season within farmOS.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
drupal_id = sa.Column(
|
||||||
|
sa.Integer(),
|
||||||
|
nullable=True,
|
||||||
|
unique=True,
|
||||||
|
doc="""
|
||||||
|
Drupal internal ID for the season.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
_plant_assets = orm.relationship(
|
||||||
|
"PlantAssetSeason",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="season",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or ""
|
||||||
|
|
||||||
|
|
||||||
class PlantAsset(AssetMixin, model.Base):
|
class PlantAsset(AssetMixin, model.Base):
|
||||||
"""
|
"""
|
||||||
Represents a plant asset from farmOS
|
Represents a plant asset from farmOS
|
||||||
|
|
@ -117,6 +176,19 @@ class PlantAsset(AssetMixin, model.Base):
|
||||||
creator=lambda pt: PlantAssetPlantType(plant_type=pt),
|
creator=lambda pt: PlantAssetPlantType(plant_type=pt),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_seasons = orm.relationship(
|
||||||
|
"PlantAssetSeason",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="plant_asset",
|
||||||
|
)
|
||||||
|
|
||||||
|
seasons = association_proxy(
|
||||||
|
"_seasons",
|
||||||
|
"season",
|
||||||
|
creator=lambda s: PlantAssetSeason(season=s),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
add_asset_proxies(PlantAsset)
|
add_asset_proxies(PlantAsset)
|
||||||
|
|
||||||
|
|
@ -146,3 +218,30 @@ class PlantAssetPlantType(model.Base):
|
||||||
""",
|
""",
|
||||||
back_populates="_plant_assets",
|
back_populates="_plant_assets",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlantAssetSeason(model.Base):
|
||||||
|
"""
|
||||||
|
Associates one or more seasons with a plant asset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "asset_plant_season"
|
||||||
|
__versioned__ = {}
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
plant_asset_uuid = model.uuid_fk_column("asset_plant.uuid", nullable=False)
|
||||||
|
plant_asset = orm.relationship(
|
||||||
|
PlantAsset,
|
||||||
|
foreign_keys=plant_asset_uuid,
|
||||||
|
back_populates="_seasons",
|
||||||
|
)
|
||||||
|
|
||||||
|
season_uuid = model.uuid_fk_column("season.uuid", nullable=False)
|
||||||
|
season = orm.relationship(
|
||||||
|
Season,
|
||||||
|
doc="""
|
||||||
|
Reference to the season.
|
||||||
|
""",
|
||||||
|
back_populates="_plant_assets",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,12 @@ class PlantTypeImporter(ToFarmOSTaxonomy):
|
||||||
farmos_taxonomy_type = "plant_type"
|
farmos_taxonomy_type = "plant_type"
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonImporter(ToFarmOSTaxonomy):
|
||||||
|
|
||||||
|
model_title = "Season"
|
||||||
|
farmos_taxonomy_type = "season"
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetImporter(ToFarmOSAsset):
|
class PlantAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
model_title = "PlantAsset"
|
model_title = "PlantAsset"
|
||||||
|
|
@ -377,6 +383,7 @@ class PlantAssetImporter(ToFarmOSAsset):
|
||||||
"uuid",
|
"uuid",
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"plant_type_uuids",
|
"plant_type_uuids",
|
||||||
|
"season_uuids",
|
||||||
"notes",
|
"notes",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
@ -388,6 +395,9 @@ class PlantAssetImporter(ToFarmOSAsset):
|
||||||
"plant_type_uuids": [
|
"plant_type_uuids": [
|
||||||
UUID(p["id"]) for p in plant["relationships"]["plant_type"]["data"]
|
UUID(p["id"]) for p in plant["relationships"]["plant_type"]["data"]
|
||||||
],
|
],
|
||||||
|
"season_uuids": [
|
||||||
|
UUID(p["id"]) for p in plant["relationships"]["season"]["data"]
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
@ -413,6 +423,15 @@ class PlantAssetImporter(ToFarmOSAsset):
|
||||||
"type": "taxonomy_term--plant_type",
|
"type": "taxonomy_term--plant_type",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if "season_uuids" in self.fields:
|
||||||
|
rels["season"] = {"data": []}
|
||||||
|
for uuid in source_data["season_uuids"]:
|
||||||
|
rels["season"]["data"].append(
|
||||||
|
{
|
||||||
|
"id": str(uuid),
|
||||||
|
"type": "taxonomy_term--season",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
payload["attributes"].update(attrs)
|
payload["attributes"].update(attrs)
|
||||||
if rels:
|
if rels:
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
||||||
importers["AnimalAsset"] = AnimalAssetImporter
|
importers["AnimalAsset"] = AnimalAssetImporter
|
||||||
importers["GroupAsset"] = GroupAssetImporter
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
importers["PlantType"] = PlantTypeImporter
|
importers["PlantType"] = PlantTypeImporter
|
||||||
|
importers["Season"] = SeasonImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
importers["PlantAsset"] = PlantAssetImporter
|
||||||
importers["Unit"] = UnitImporter
|
importers["Unit"] = UnitImporter
|
||||||
importers["StandardQuantity"] = StandardQuantityImporter
|
importers["StandardQuantity"] = StandardQuantityImporter
|
||||||
|
|
@ -316,6 +317,28 @@ class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonImporter(FromWuttaFarm, farmos_importing.model.SeasonImporter):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Seasons
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.Season
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
drupal_internal_id_field = "drupal_internal__tid"
|
||||||
|
|
||||||
|
def normalize_source_object(self, season):
|
||||||
|
return {
|
||||||
|
"uuid": season.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"name": season.name,
|
||||||
|
"_src_object": season,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetImporter(FromWuttaFarmAsset, 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
|
||||||
|
|
@ -328,6 +351,7 @@ class PlantAssetImporter(FromWuttaFarmAsset, farmos_importing.model.PlantAssetIm
|
||||||
fields.extend(
|
fields.extend(
|
||||||
[
|
[
|
||||||
"plant_type_uuids",
|
"plant_type_uuids",
|
||||||
|
"season_uuids",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
@ -336,9 +360,8 @@ class PlantAssetImporter(FromWuttaFarmAsset, farmos_importing.model.PlantAssetIm
|
||||||
data = super().normalize_source_object(plant)
|
data = super().normalize_source_object(plant)
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"plant_type_uuids": [
|
"plant_type_uuids": [pt.farmos_uuid for pt in plant.plant_types],
|
||||||
t.plant_type.farmos_uuid for t in plant._plant_types
|
"season_uuids": [s.farmos_uuid for s in plant.seasons],
|
||||||
],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
|
||||||
importers["AnimalAsset"] = AnimalAssetImporter
|
importers["AnimalAsset"] = AnimalAssetImporter
|
||||||
importers["GroupAsset"] = GroupAssetImporter
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
importers["PlantType"] = PlantTypeImporter
|
importers["PlantType"] = PlantTypeImporter
|
||||||
|
importers["Season"] = SeasonImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
importers["PlantAsset"] = PlantAssetImporter
|
||||||
importers["Measure"] = MeasureImporter
|
importers["Measure"] = MeasureImporter
|
||||||
importers["Unit"] = UnitImporter
|
importers["Unit"] = UnitImporter
|
||||||
|
|
@ -577,6 +578,34 @@ class PlantTypeImporter(FromFarmOS, ToWutta):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonImporter(FromFarmOS, ToWutta):
|
||||||
|
"""
|
||||||
|
farmOS API → WuttaFarm importer for Seasons
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = model.Season
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"farmos_uuid",
|
||||||
|
"drupal_id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_source_objects(self):
|
||||||
|
""" """
|
||||||
|
return list(self.farmos_client.resource.iterate("taxonomy_term", "season"))
|
||||||
|
|
||||||
|
def normalize_source_object(self, season):
|
||||||
|
""" """
|
||||||
|
return {
|
||||||
|
"farmos_uuid": UUID(season["id"]),
|
||||||
|
"drupal_id": season["attributes"]["drupal_internal__tid"],
|
||||||
|
"name": season["attributes"]["name"],
|
||||||
|
"description": season["attributes"]["description"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetImporter(AssetImporterBase):
|
class PlantAssetImporter(AssetImporterBase):
|
||||||
"""
|
"""
|
||||||
farmOS API → WuttaFarm importer for Plant Assets
|
farmOS API → WuttaFarm importer for Plant Assets
|
||||||
|
|
@ -589,6 +618,7 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
fields.extend(
|
fields.extend(
|
||||||
[
|
[
|
||||||
"plant_types",
|
"plant_types",
|
||||||
|
"seasons",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
@ -602,11 +632,17 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
if plant_type.farmos_uuid:
|
if plant_type.farmos_uuid:
|
||||||
self.plant_types_by_farmos_uuid[plant_type.farmos_uuid] = plant_type
|
self.plant_types_by_farmos_uuid[plant_type.farmos_uuid] = plant_type
|
||||||
|
|
||||||
|
self.seasons_by_farmos_uuid = {}
|
||||||
|
for season in self.target_session.query(model.Season):
|
||||||
|
if season.farmos_uuid:
|
||||||
|
self.seasons_by_farmos_uuid[season.farmos_uuid] = season
|
||||||
|
|
||||||
def normalize_source_object(self, plant):
|
def normalize_source_object(self, plant):
|
||||||
""" """
|
""" """
|
||||||
data = super().normalize_source_object(plant)
|
data = super().normalize_source_object(plant)
|
||||||
|
|
||||||
plant_types = []
|
plant_types = []
|
||||||
|
seasons = []
|
||||||
if relationships := plant.get("relationships"):
|
if relationships := plant.get("relationships"):
|
||||||
|
|
||||||
if plant_type := relationships.get("plant_type"):
|
if plant_type := relationships.get("plant_type"):
|
||||||
|
|
@ -619,9 +655,18 @@ 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"])
|
||||||
|
|
||||||
|
if season := relationships.get("season"):
|
||||||
|
seasons = []
|
||||||
|
for season in season["data"]:
|
||||||
|
if wf_season := self.seasons_by_farmos_uuid.get(UUID(season["id"])):
|
||||||
|
seasons.append(wf_season.uuid)
|
||||||
|
else:
|
||||||
|
log.warning("season not found: %s", season["id"])
|
||||||
|
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"plant_types": set(plant_types),
|
"plant_types": set(plant_types),
|
||||||
|
"seasons": set(seasons),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
@ -632,6 +677,9 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
if "plant_types" in self.fields:
|
if "plant_types" in self.fields:
|
||||||
data["plant_types"] = set([pt.uuid for pt in plant.plant_types])
|
data["plant_types"] = set([pt.uuid for pt in plant.plant_types])
|
||||||
|
|
||||||
|
if "seasons" in self.fields:
|
||||||
|
data["seasons"] = set([s.uuid for s in plant.seasons])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def update_target_object(self, plant, source_data, target_data=None):
|
def update_target_object(self, plant, source_data, target_data=None):
|
||||||
|
|
@ -664,6 +712,25 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
)
|
)
|
||||||
self.target_session.delete(plant_type)
|
self.target_session.delete(plant_type)
|
||||||
|
|
||||||
|
if "seasons" in self.fields:
|
||||||
|
if not target_data or target_data["seasons"] != source_data["seasons"]:
|
||||||
|
|
||||||
|
for uuid in source_data["seasons"]:
|
||||||
|
if not target_data or uuid not in target_data["seasons"]:
|
||||||
|
self.target_session.flush()
|
||||||
|
plant._seasons.append(model.PlantAssetSeason(season_uuid=uuid))
|
||||||
|
|
||||||
|
if target_data:
|
||||||
|
for uuid in target_data["seasons"]:
|
||||||
|
if uuid not in source_data["seasons"]:
|
||||||
|
season = (
|
||||||
|
self.target_session.query(model.PlantAssetSeason)
|
||||||
|
.filter(model.PlantAssetSeason.plant_asset == plant)
|
||||||
|
.filter(model.PlantAssetSeason.season_uuid == uuid)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
self.target_session.delete(season)
|
||||||
|
|
||||||
return plant
|
return plant
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ class Normalizer(GenericHandler):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# if self.farmos_4x:
|
||||||
|
# archived = asset["attributes"]["archived"]
|
||||||
|
# else:
|
||||||
|
# archived = asset["attributes"]["status"] == "archived"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": asset["id"],
|
"uuid": asset["id"],
|
||||||
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,31 @@ class PlantTypeRefs(WuttaSet):
|
||||||
return PlantTypeRefsWidget(self.request, **kwargs)
|
return PlantTypeRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonRefs(WuttaSet):
|
||||||
|
"""
|
||||||
|
Schema type for Plant Types field (on a Plant Asset).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, node, appstruct):
|
||||||
|
if not appstruct:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [season.uuid.hex for season in appstruct]
|
||||||
|
|
||||||
|
def widget_maker(self, **kwargs):
|
||||||
|
from wuttafarm.web.forms.widgets import SeasonRefsWidget
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
if "values" not in kwargs:
|
||||||
|
seasons = session.query(model.Season).order_by(model.Season.name).all()
|
||||||
|
values = [(s.uuid.hex, str(s)) for s in seasons]
|
||||||
|
kwargs["values"] = values
|
||||||
|
|
||||||
|
return SeasonRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class StructureType(colander.SchemaType):
|
class StructureType(colander.SchemaType):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,78 @@ class PlantTypeRefsWidget(Widget):
|
||||||
return set(pstruct.split(","))
|
return set(pstruct.split(","))
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonRefsWidget(Widget):
|
||||||
|
"""
|
||||||
|
Widget for Seasons field (on a Plant Asset).
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = "seasonrefs"
|
||||||
|
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):
|
||||||
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
if cstruct in (colander.null, None):
|
||||||
|
cstruct = ()
|
||||||
|
|
||||||
|
if readonly := kw.get("readonly", self.readonly):
|
||||||
|
items = []
|
||||||
|
|
||||||
|
seasons = (
|
||||||
|
session.query(model.Season)
|
||||||
|
.filter(model.Season.uuid.in_(cstruct))
|
||||||
|
.order_by(model.Season.name)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
for season in seasons:
|
||||||
|
items.append(
|
||||||
|
HTML.tag(
|
||||||
|
"li",
|
||||||
|
c=tags.link_to(
|
||||||
|
str(season),
|
||||||
|
self.request.route_url("seasons.view", uuid=season.uuid),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTML.tag("ul", c=items)
|
||||||
|
|
||||||
|
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"])
|
||||||
|
|
||||||
|
if self.request.has_perm("seasons.create"):
|
||||||
|
values["can_create"] = True
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def deserialize(self, field, pstruct):
|
||||||
|
""" """
|
||||||
|
if not pstruct:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
return set(pstruct.split(","))
|
||||||
|
|
||||||
|
|
||||||
class StructureWidget(Widget):
|
class StructureWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget to display a "structure" field.
|
Widget to display a "structure" field.
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "plant_types",
|
"route": "plant_types",
|
||||||
"perm": "plant_types.list",
|
"perm": "plant_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seasons",
|
||||||
|
"route": "seasons",
|
||||||
|
"perm": "seasons.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Structure Types",
|
"title": "Structure Types",
|
||||||
"route": "structure_types",
|
"route": "structure_types",
|
||||||
|
|
@ -281,6 +286,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_plant_types",
|
"route": "farmos_plant_types",
|
||||||
"perm": "farmos_plant_types.list",
|
"perm": "farmos_plant_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seasons",
|
||||||
|
"route": "farmos_seasons",
|
||||||
|
"perm": "farmos_seasons.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Structure Types",
|
"title": "Structure Types",
|
||||||
"route": "farmos_structure_types",
|
"route": "farmos_structure_types",
|
||||||
|
|
@ -369,6 +379,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_plant_types",
|
"route": "farmos_plant_types",
|
||||||
"perm": "farmos_plant_types.list",
|
"perm": "farmos_plant_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seasons",
|
||||||
|
"route": "farmos_seasons",
|
||||||
|
"perm": "farmos_seasons.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Structure Types",
|
"title": "Structure Types",
|
||||||
"route": "farmos_structure_types",
|
"route": "farmos_structure_types",
|
||||||
|
|
|
||||||
13
src/wuttafarm/web/templates/deform/seasonrefs.pt
Normal file
13
src/wuttafarm/web/templates/deform/seasonrefs.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="">
|
||||||
|
|
||||||
|
<seasons-picker tal:attributes="name name;
|
||||||
|
v-model vmodel;
|
||||||
|
:seasons js_values;
|
||||||
|
:can-create str(can_create).lower();" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
${self.make_assets_picker_component()}
|
${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()}
|
||||||
|
${self.make_seasons_picker_component()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="make_assets_picker_component()">
|
<%def name="make_assets_picker_component()">
|
||||||
|
|
@ -433,3 +434,199 @@
|
||||||
<% request.register_component('plant-types-picker', 'PlantTypesPicker') %>
|
<% request.register_component('plant-types-picker', 'PlantTypesPicker') %>
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_seasons_picker_component()">
|
||||||
|
<script type="text/x-template" id="seasons-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>
|
||||||
|
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="plus"
|
||||||
|
@click="createInit()">
|
||||||
|
New
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<${b}-table :data="seasonData">
|
||||||
|
|
||||||
|
<${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="removeSeason(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 Season</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 SeasonsPicker = {
|
||||||
|
template: '#seasons-picker-template',
|
||||||
|
mixins: [WuttaRequestMixin],
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
value: Array,
|
||||||
|
seasons: Array,
|
||||||
|
canCreate: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
internalSeasons: this.seasons.map((pt) => {
|
||||||
|
return {uuid: pt[0], name: pt[1]}
|
||||||
|
}),
|
||||||
|
|
||||||
|
addName: '',
|
||||||
|
|
||||||
|
createShowDialog: false,
|
||||||
|
createName: null,
|
||||||
|
createSaving: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
seasonData() {
|
||||||
|
const data = []
|
||||||
|
|
||||||
|
if (this.value) {
|
||||||
|
for (let ptype of this.internalSeasons) {
|
||||||
|
if (this.value.includes(ptype.uuid)) {
|
||||||
|
data.push(ptype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
addNameData() {
|
||||||
|
if (!this.addName) {
|
||||||
|
return this.internalSeasons
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.internalSeasons.filter((ptype) => {
|
||||||
|
return ptype.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
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
## TODO
|
||||||
|
% if not app.is_farmos_wrapper():
|
||||||
|
const url = "${url('seasons.ajax_create')}"
|
||||||
|
% endif
|
||||||
|
const params = {name: this.createName}
|
||||||
|
this.wuttaPOST(url, params, response => {
|
||||||
|
this.internalSeasons.push(response.data)
|
||||||
|
const value = Array.from(this.value || [])
|
||||||
|
value.push(response.data.uuid)
|
||||||
|
this.$emit('input', value)
|
||||||
|
this.addName = null
|
||||||
|
this.createSaving = false
|
||||||
|
this.createShowDialog = false
|
||||||
|
}, response => {
|
||||||
|
this.createSaving = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSeason(ptype) {
|
||||||
|
let value = Array.from(this.value)
|
||||||
|
const i = value.indexOf(ptype.uuid)
|
||||||
|
value.splice(i, 1)
|
||||||
|
this.$emit('input', value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Vue.component('seasons-picker', SeasonsPicker)
|
||||||
|
<% request.register_component('seasons-picker', 'SeasonsPicker') %>
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ class CommonView(base.CommonView):
|
||||||
"farmos_quantities_standard.view",
|
"farmos_quantities_standard.view",
|
||||||
"farmos_quantity_types.list",
|
"farmos_quantity_types.list",
|
||||||
"farmos_quantity_types.view",
|
"farmos_quantity_types.view",
|
||||||
|
"farmos_seasons.list",
|
||||||
|
"farmos_seasons.view",
|
||||||
"farmos_structure_assets.list",
|
"farmos_structure_assets.list",
|
||||||
"farmos_structure_assets.view",
|
"farmos_structure_assets.view",
|
||||||
"farmos_structure_types.list",
|
"farmos_structure_types.list",
|
||||||
|
|
@ -132,6 +134,12 @@ class CommonView(base.CommonView):
|
||||||
"logs_observation.view",
|
"logs_observation.view",
|
||||||
"logs_observation.versions",
|
"logs_observation.versions",
|
||||||
"quick.eggs",
|
"quick.eggs",
|
||||||
|
"plant_types.list",
|
||||||
|
"plant_types.view",
|
||||||
|
"plant_types.versions",
|
||||||
|
"seasons.list",
|
||||||
|
"seasons.view",
|
||||||
|
"seasons.versions",
|
||||||
"structure_types.list",
|
"structure_types.list",
|
||||||
"structure_types.view",
|
"structure_types.view",
|
||||||
"structure_types.versions",
|
"structure_types.versions",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,12 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
from wuttafarm.web.forms.schema import UsersType, StructureType, FarmOSPlantTypes
|
from wuttafarm.web.forms.schema import (
|
||||||
|
UsersType,
|
||||||
|
StructureType,
|
||||||
|
FarmOSPlantTypes,
|
||||||
|
FarmOSRefs,
|
||||||
|
)
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -75,6 +80,43 @@ class PlantTypeView(TaxonomyMasterView):
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonView(TaxonomyMasterView):
|
||||||
|
"""
|
||||||
|
Master view for Seasons in farmOS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_name = "farmos_season"
|
||||||
|
model_title = "farmOS Season"
|
||||||
|
model_title_plural = "farmOS Seasons"
|
||||||
|
|
||||||
|
route_prefix = "farmos_seasons"
|
||||||
|
url_prefix = "/farmOS/seasons"
|
||||||
|
|
||||||
|
farmos_taxonomy_type = "season"
|
||||||
|
farmos_refurl_path = "/admin/structure/taxonomy/manage/season/overview"
|
||||||
|
|
||||||
|
def get_xref_buttons(self, season):
|
||||||
|
buttons = super().get_xref_buttons(season)
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
|
||||||
|
if wf_season := (
|
||||||
|
session.query(model.Season)
|
||||||
|
.filter(model.Season.farmos_uuid == season["uuid"])
|
||||||
|
.first()
|
||||||
|
):
|
||||||
|
buttons.append(
|
||||||
|
self.make_button(
|
||||||
|
f"View {self.app.get_title()} record",
|
||||||
|
primary=True,
|
||||||
|
url=self.request.route_url("seasons.view", uuid=wf_season.uuid),
|
||||||
|
icon_left="eye",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetView(FarmOSMasterView):
|
class PlantAssetView(FarmOSMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for farmOS Plant Assets
|
Master view for farmOS Plant Assets
|
||||||
|
|
@ -89,6 +131,10 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
|
|
||||||
farmos_refurl_path = "/assets/plant"
|
farmos_refurl_path = "/assets/plant"
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"seasons": "Season",
|
||||||
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"name",
|
"name",
|
||||||
"archived",
|
"archived",
|
||||||
|
|
@ -99,6 +145,7 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"plant_types",
|
"plant_types",
|
||||||
|
"seasons",
|
||||||
"archived",
|
"archived",
|
||||||
"owners",
|
"owners",
|
||||||
"location",
|
"location",
|
||||||
|
|
@ -151,6 +198,21 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# add seasons
|
||||||
|
if seasons := relationships.get("season"):
|
||||||
|
if seasons["data"]:
|
||||||
|
data["seasons"] = []
|
||||||
|
for season in seasons["data"]:
|
||||||
|
season = self.farmos_client.resource.get_id(
|
||||||
|
"taxonomy_term", "season", season["id"]
|
||||||
|
)
|
||||||
|
data["seasons"].append(
|
||||||
|
{
|
||||||
|
"uuid": season["data"]["id"],
|
||||||
|
"name": season["data"]["attributes"]["name"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# add location
|
# add location
|
||||||
if location := relationships.get("location"):
|
if location := relationships.get("location"):
|
||||||
if location["data"]:
|
if location["data"]:
|
||||||
|
|
@ -199,22 +261,14 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
return plant["name"]
|
return plant["name"]
|
||||||
|
|
||||||
def normalize_plant(self, plant):
|
def normalize_plant(self, plant):
|
||||||
|
normal = self.normal.normalize_farmos_asset(plant)
|
||||||
if notes := plant["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
if self.farmos_4x:
|
|
||||||
archived = plant["attributes"]["archived"]
|
|
||||||
else:
|
|
||||||
archived = plant["attributes"]["status"] == "archived"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": plant["id"],
|
"uuid": normal["uuid"],
|
||||||
"drupal_id": plant["attributes"]["drupal_internal__id"],
|
"drupal_id": normal["drupal_id"],
|
||||||
"name": plant["attributes"]["name"],
|
"name": normal["asset_name"],
|
||||||
"location": colander.null, # TODO
|
"location": colander.null, # TODO
|
||||||
"archived": archived,
|
"archived": normal["archived"],
|
||||||
"notes": notes or colander.null,
|
"notes": normal["notes"] or colander.null,
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
|
|
@ -225,6 +279,9 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
# plant_types
|
# plant_types
|
||||||
f.set_node("plant_types", FarmOSPlantTypes(self.request))
|
f.set_node("plant_types", FarmOSPlantTypes(self.request))
|
||||||
|
|
||||||
|
# seasons
|
||||||
|
f.set_node("seasons", FarmOSRefs(self.request, "farmos_seasons"))
|
||||||
|
|
||||||
# location
|
# location
|
||||||
f.set_node("location", StructureType(self.request))
|
f.set_node("location", StructureType(self.request))
|
||||||
|
|
||||||
|
|
@ -279,6 +336,9 @@ def defaults(config, **kwargs):
|
||||||
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
||||||
PlantTypeView.defaults(config)
|
PlantTypeView.defaults(config)
|
||||||
|
|
||||||
|
SeasonView = kwargs.get("SeasonView", base["SeasonView"])
|
||||||
|
SeasonView.defaults(config)
|
||||||
|
|
||||||
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
||||||
PlantAssetView.defaults(config)
|
PlantAssetView.defaults(config)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ from webhelpers2.html import tags
|
||||||
from wuttaweb.forms.schema import WuttaDictEnum
|
from wuttaweb.forms.schema import WuttaDictEnum
|
||||||
from wuttaweb.util import get_form_data
|
from wuttaweb.util import get_form_data
|
||||||
|
|
||||||
from wuttafarm.db.model import PlantType, PlantAsset
|
from wuttafarm.db.model import PlantType, Season, PlantAsset
|
||||||
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
||||||
from wuttafarm.web.forms.schema import PlantTypeRefs
|
from wuttafarm.web.forms.schema import PlantTypeRefs, SeasonRefs
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
from wuttafarm.web.util import get_farmos_client_for_user
|
from wuttafarm.web.util import get_farmos_client_for_user
|
||||||
|
|
||||||
|
|
@ -195,6 +195,166 @@ class PlantTypeView(AssetTypeMasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonView(AssetTypeMasterView):
|
||||||
|
"""
|
||||||
|
Master view for Seasons
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = Season
|
||||||
|
route_prefix = "seasons"
|
||||||
|
url_prefix = "/seasons"
|
||||||
|
|
||||||
|
farmos_entity_type = "taxonomy_term"
|
||||||
|
farmos_bundle = "season"
|
||||||
|
farmos_refurl_path = "/admin/structure/taxonomy/manage/season/overview"
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
sort_defaults = "name"
|
||||||
|
|
||||||
|
filter_defaults = {
|
||||||
|
"name": {"active": True, "verb": "contains"},
|
||||||
|
}
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
row_model_class = PlantAsset
|
||||||
|
rows_viewable = True
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
"asset_name",
|
||||||
|
"archived",
|
||||||
|
]
|
||||||
|
|
||||||
|
rows_sort_defaults = "asset_name"
|
||||||
|
|
||||||
|
def configure_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# name
|
||||||
|
g.set_link("name")
|
||||||
|
|
||||||
|
def get_farmos_url(self, season):
|
||||||
|
return self.app.get_farmos_url(f"/taxonomy/term/{season.drupal_id}")
|
||||||
|
|
||||||
|
def get_xref_buttons(self, season):
|
||||||
|
buttons = super().get_xref_buttons(season)
|
||||||
|
|
||||||
|
if season.farmos_uuid:
|
||||||
|
buttons.append(
|
||||||
|
self.make_button(
|
||||||
|
"View farmOS record",
|
||||||
|
primary=True,
|
||||||
|
url=self.request.route_url(
|
||||||
|
"farmos_seasons.view", uuid=season.farmos_uuid
|
||||||
|
),
|
||||||
|
icon_left="eye",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
season = self.get_instance()
|
||||||
|
|
||||||
|
if season._plant_assets:
|
||||||
|
self.request.session.flash(
|
||||||
|
"Cannot delete season which is still referenced by plant assets.",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
url = self.get_action_url("view", season)
|
||||||
|
return self.redirect(self.request.get_referrer(default=url))
|
||||||
|
|
||||||
|
return super().delete()
|
||||||
|
|
||||||
|
def get_row_grid_data(self, season):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
return (
|
||||||
|
session.query(model.PlantAsset)
|
||||||
|
.join(model.Asset)
|
||||||
|
.outerjoin(model.PlantAssetSeason)
|
||||||
|
.filter(model.PlantAssetSeason.season == season)
|
||||||
|
)
|
||||||
|
|
||||||
|
def configure_row_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_row_grid(g)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# asset_name
|
||||||
|
g.set_link("asset_name")
|
||||||
|
g.set_sorter("asset_name", model.Asset.asset_name)
|
||||||
|
g.set_filter("asset_name", model.Asset.asset_name)
|
||||||
|
|
||||||
|
# archived
|
||||||
|
g.set_renderer("archived", "boolean")
|
||||||
|
g.set_sorter("archived", model.Asset.archived)
|
||||||
|
g.set_filter("archived", model.Asset.archived)
|
||||||
|
|
||||||
|
def get_row_action_url_view(self, plant, i):
|
||||||
|
return self.request.route_url("plant_assets.view", uuid=plant.uuid)
|
||||||
|
|
||||||
|
def ajax_create(self):
|
||||||
|
"""
|
||||||
|
AJAX view to create a new season.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
data = get_form_data(self.request)
|
||||||
|
|
||||||
|
name = data.get("name")
|
||||||
|
if not name:
|
||||||
|
return {"error": "Name is required"}
|
||||||
|
|
||||||
|
season = model.Season(name=name)
|
||||||
|
session.add(season)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
if self.app.is_farmos_mirror():
|
||||||
|
client = get_farmos_client_for_user(self.request)
|
||||||
|
self.app.auto_sync_to_farmos(season, client=client)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uuid": season.uuid.hex,
|
||||||
|
"name": season.name,
|
||||||
|
"farmos_uuid": season.farmos_uuid.hex,
|
||||||
|
"drupal_id": season.drupal_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
""" """
|
||||||
|
cls._defaults(config)
|
||||||
|
cls._season_defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _season_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
url_prefix = cls.get_url_prefix()
|
||||||
|
|
||||||
|
# ajax_create
|
||||||
|
config.add_route(f"{route_prefix}.ajax_create", f"{url_prefix}/ajax/new")
|
||||||
|
config.add_view(
|
||||||
|
cls,
|
||||||
|
attr="ajax_create",
|
||||||
|
route_name=f"{route_prefix}.ajax_create",
|
||||||
|
permission=f"{permission_prefix}.create",
|
||||||
|
renderer="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetView(AssetMasterView):
|
class PlantAssetView(AssetMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Plant Assets
|
Master view for Plant Assets
|
||||||
|
|
@ -209,6 +369,7 @@ class PlantAssetView(AssetMasterView):
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"plant_types": "Crop/Variety",
|
"plant_types": "Crop/Variety",
|
||||||
|
"seasons": "Season",
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
|
@ -254,17 +415,26 @@ class PlantAssetView(AssetMasterView):
|
||||||
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 not (self.creating or self.editing): # TODO
|
f.fields.insert_after("plant_types", "seasons")
|
||||||
f.fields.insert_after("plant_types", "season")
|
f.set_node("seasons", SeasonRefs(self.request))
|
||||||
|
f.set_required("seasons", False)
|
||||||
|
if not self.creating:
|
||||||
|
# nb. must explcitly declare value for non-standard field
|
||||||
|
f.set_default("seasons", plant.seasons)
|
||||||
|
|
||||||
def objectify(self, form):
|
def objectify(self, form):
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
plant = super().objectify(form)
|
plant = super().objectify(form)
|
||||||
data = form.validated
|
data = form.validated
|
||||||
|
|
||||||
|
self.set_plant_types(plant, data["plant_types"])
|
||||||
|
self.set_seasons(plant, data["seasons"])
|
||||||
|
|
||||||
|
return plant
|
||||||
|
|
||||||
|
def set_plant_types(self, plant, desired):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
current = [pt.uuid for pt in plant.plant_types]
|
current = [pt.uuid for pt in plant.plant_types]
|
||||||
desired = data["plant_types"]
|
|
||||||
|
|
||||||
for uuid in desired:
|
for uuid in desired:
|
||||||
if uuid not in current:
|
if uuid not in current:
|
||||||
|
|
@ -278,7 +448,22 @@ class PlantAssetView(AssetMasterView):
|
||||||
assert plant_type
|
assert plant_type
|
||||||
plant.plant_types.remove(plant_type)
|
plant.plant_types.remove(plant_type)
|
||||||
|
|
||||||
return plant
|
def set_seasons(self, plant, desired):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
current = [s.uuid for s in plant.seasons]
|
||||||
|
|
||||||
|
for uuid in desired:
|
||||||
|
if uuid not in current:
|
||||||
|
season = session.get(model.Season, uuid)
|
||||||
|
assert season
|
||||||
|
plant.seasons.append(season)
|
||||||
|
|
||||||
|
for uuid in current:
|
||||||
|
if uuid not in desired:
|
||||||
|
season = session.get(model.Season, uuid)
|
||||||
|
assert season
|
||||||
|
plant.seasons.remove(season)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
|
|
@ -287,6 +472,9 @@ def defaults(config, **kwargs):
|
||||||
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
||||||
PlantTypeView.defaults(config)
|
PlantTypeView.defaults(config)
|
||||||
|
|
||||||
|
SeasonView = kwargs.get("SeasonView", base["SeasonView"])
|
||||||
|
SeasonView.defaults(config)
|
||||||
|
|
||||||
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
||||||
PlantAssetView.defaults(config)
|
PlantAssetView.defaults(config)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue