Compare commits
5 commits
b2b49d93ae
...
af2ea18e1d
| Author | SHA1 | Date | |
|---|---|---|---|
| af2ea18e1d | |||
| 23af35842d | |||
| 609a900f39 | |||
| a547188a90 | |||
| 81fa22bbd8 |
12 changed files with 600 additions and 44 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,6 +5,19 @@ All notable changes to WuttaFarm will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v0.8.0 (2026-03-04)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- improve support for exporting quantity, log data
|
||||||
|
- show related Quantity records when viewing a Measure
|
||||||
|
- show related Quantity records when viewing a Unit
|
||||||
|
- show link to Log record when viewing Quantity
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- bump version requirement for wuttaweb
|
||||||
|
|
||||||
## v0.7.0 (2026-03-04)
|
## v0.7.0 (2026-03-04)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaFarm"
|
name = "WuttaFarm"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
description = "Web app to integrate with and extend farmOS"
|
description = "Web app to integrate with and extend farmOS"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@ class LogQuantity(model.Base):
|
||||||
quantity = orm.relationship(
|
quantity = orm.relationship(
|
||||||
"Quantity",
|
"Quantity",
|
||||||
foreign_keys=quantity_uuid,
|
foreign_keys=quantity_uuid,
|
||||||
|
back_populates="_log",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ Model definition for Quantities
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
|
||||||
from wuttjamaican.db import model
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
@ -161,6 +162,25 @@ class Quantity(model.Base):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_log = orm.relationship(
|
||||||
|
"LogQuantity",
|
||||||
|
uselist=False,
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="quantity",
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_log_quantity(log):
|
||||||
|
from wuttafarm.db.model import LogQuantity
|
||||||
|
|
||||||
|
return LogQuantity(log=log)
|
||||||
|
|
||||||
|
log = association_proxy(
|
||||||
|
"_log",
|
||||||
|
"log",
|
||||||
|
creator=make_log_quantity,
|
||||||
|
)
|
||||||
|
|
||||||
def render_as_text(self, config=None):
|
def render_as_text(self, config=None):
|
||||||
measure = str(self.measure or self.measure_id or "")
|
measure = str(self.measure or self.measure_id or "")
|
||||||
value = self.value_numerator / self.value_denominator
|
value = self.value_numerator / self.value_denominator
|
||||||
|
|
@ -202,6 +222,7 @@ def add_quantity_proxies(subclass):
|
||||||
Quantity.make_proxy(subclass, "quantity", "units_uuid")
|
Quantity.make_proxy(subclass, "quantity", "units_uuid")
|
||||||
Quantity.make_proxy(subclass, "quantity", "units")
|
Quantity.make_proxy(subclass, "quantity", "units")
|
||||||
Quantity.make_proxy(subclass, "quantity", "label")
|
Quantity.make_proxy(subclass, "quantity", "label")
|
||||||
|
Quantity.make_proxy(subclass, "quantity", "log")
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantity(QuantityMixin, model.Base):
|
class StandardQuantity(QuantityMixin, model.Base):
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,138 @@ class StructureAssetImporter(ToFarmOSAsset):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# quantity importers
|
||||||
|
##############################
|
||||||
|
|
||||||
|
|
||||||
|
class ToFarmOSQuantity(ToFarmOS):
|
||||||
|
"""
|
||||||
|
Base class for quantity data importer targeting the farmOS API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
farmos_quantity_type = None
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"measure",
|
||||||
|
"value_numerator",
|
||||||
|
"value_denominator",
|
||||||
|
"label",
|
||||||
|
"quantity_type_uuid",
|
||||||
|
"unit_uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_target_objects(self, **kwargs):
|
||||||
|
return list(
|
||||||
|
self.farmos_client.resource.iterate("quantity", self.farmos_quantity_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
# fetch from cache, if applicable
|
||||||
|
if self.caches_target:
|
||||||
|
return super().get_target_object(key)
|
||||||
|
|
||||||
|
# okay now must fetch via API
|
||||||
|
if self.get_keys() != ["uuid"]:
|
||||||
|
raise ValueError("must use uuid key for this to work")
|
||||||
|
uuid = key[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
qty = self.farmos_client.resource.get_id(
|
||||||
|
"quantity", self.farmos_quantity_type, str(uuid)
|
||||||
|
)
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
return qty["data"]
|
||||||
|
|
||||||
|
def create_target_object(self, key, source_data):
|
||||||
|
if source_data.get("__ignoreme__"):
|
||||||
|
return None
|
||||||
|
if self.dry_run:
|
||||||
|
return source_data
|
||||||
|
|
||||||
|
payload = self.get_quantity_payload(source_data)
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"quantity", self.farmos_quantity_type, payload
|
||||||
|
)
|
||||||
|
normal = self.normalize_target_object(result["data"])
|
||||||
|
normal["_new_object"] = result["data"]
|
||||||
|
return normal
|
||||||
|
|
||||||
|
def update_target_object(self, quantity, source_data, target_data=None):
|
||||||
|
if self.dry_run:
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
payload = self.get_quantity_payload(source_data)
|
||||||
|
payload["id"] = str(source_data["uuid"])
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"quantity", self.farmos_quantity_type, payload
|
||||||
|
)
|
||||||
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
|
def normalize_target_object(self, qty):
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"uuid": UUID(qty["id"]),
|
||||||
|
"measure": qty["attributes"]["measure"],
|
||||||
|
"value_numerator": qty["attributes"]["value"]["numerator"],
|
||||||
|
"value_denominator": qty["attributes"]["value"]["denominator"],
|
||||||
|
"label": qty["attributes"]["label"],
|
||||||
|
"quantity_type_uuid": UUID(
|
||||||
|
qty["relationships"]["quantity_type"]["data"]["id"]
|
||||||
|
),
|
||||||
|
"unit_uuid": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit := qty["relationships"]["units"]["data"]:
|
||||||
|
result["unit_uuid"] = UUID(unit["id"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_quantity_payload(self, source_data):
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if "measure" in self.fields:
|
||||||
|
attrs["measure"] = source_data["measure"]
|
||||||
|
if "value_numerator" in self.fields and "value_denominator" in self.fields:
|
||||||
|
attrs["value"] = {
|
||||||
|
"numerator": source_data["value_numerator"],
|
||||||
|
"denominator": source_data["value_denominator"],
|
||||||
|
}
|
||||||
|
if "label" in self.fields:
|
||||||
|
attrs["label"] = source_data["label"]
|
||||||
|
|
||||||
|
rels = {}
|
||||||
|
if "quantity_type_uuid" in self.fields:
|
||||||
|
rels["quantity_type"] = {
|
||||||
|
"data": {
|
||||||
|
"id": str(source_data["quantity_type_uuid"]),
|
||||||
|
"type": "quantity_type--quantity_type",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if "unit_uuid" in self.fields:
|
||||||
|
rels["units"] = {
|
||||||
|
"data": {
|
||||||
|
"id": str(source_data["unit_uuid"]),
|
||||||
|
"type": "taxonomy_term--unit",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"attributes": attrs, "relationships": rels}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class StandardQuantityImporter(ToFarmOSQuantity):
|
||||||
|
|
||||||
|
model_title = "StandardQuantity"
|
||||||
|
farmos_quantity_type = "standard"
|
||||||
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# log importers
|
# log importers
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -464,8 +596,14 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
"status",
|
"status",
|
||||||
"notes",
|
"notes",
|
||||||
"quick",
|
"quick",
|
||||||
|
"assets",
|
||||||
|
"quantities",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.normal = self.app.get_normalizer(self.farmos_client)
|
||||||
|
|
||||||
def get_target_objects(self, **kwargs):
|
def get_target_objects(self, **kwargs):
|
||||||
result = self.farmos_client.log.get(self.farmos_log_type)
|
result = self.farmos_client.log.get(self.farmos_log_type)
|
||||||
return result["data"]
|
return result["data"]
|
||||||
|
|
@ -511,19 +649,18 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
return self.normalize_target_object(result["data"])
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
def normalize_target_object(self, log):
|
def normalize_target_object(self, log):
|
||||||
|
normal = self.normal.normalize_farmos_log(log)
|
||||||
if notes := log["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": UUID(log["id"]),
|
"uuid": UUID(normal["uuid"]),
|
||||||
"name": log["attributes"]["name"],
|
"name": normal["name"],
|
||||||
"timestamp": self.normalize_datetime(log["attributes"]["timestamp"]),
|
"timestamp": self.app.make_utc(normal["timestamp"]),
|
||||||
"is_movement": log["attributes"]["is_movement"],
|
"is_movement": normal["is_movement"],
|
||||||
"is_group_assignment": log["attributes"]["is_group_assignment"],
|
"is_group_assignment": normal["is_group_assignment"],
|
||||||
"status": log["attributes"]["status"],
|
"status": normal["status"],
|
||||||
"notes": notes,
|
"notes": normal["notes"],
|
||||||
"quick": log["attributes"]["quick"],
|
"quick": normal["quick"],
|
||||||
|
"assets": [(a["asset_type"], UUID(a["uuid"])) for a in normal["assets"]],
|
||||||
|
"quantities": [UUID(uuid) for uuid in normal["quantity_uuids"]],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_log_payload(self, source_data):
|
def get_log_payload(self, source_data):
|
||||||
|
|
@ -542,10 +679,32 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
if "notes" in self.fields:
|
if "notes" in self.fields:
|
||||||
attrs["notes"] = {"value": source_data["notes"]}
|
attrs["notes"] = {"value": source_data["notes"]}
|
||||||
if "quick" in self.fields:
|
if "quick" in self.fields:
|
||||||
attrs["quick"] = {"value": source_data["quick"]}
|
attrs["quick"] = source_data["quick"]
|
||||||
|
|
||||||
payload = {"attributes": attrs}
|
rels = {}
|
||||||
|
if "assets" in self.fields:
|
||||||
|
assets = []
|
||||||
|
for asset_type, uuid in source_data["assets"]:
|
||||||
|
assets.append(
|
||||||
|
{
|
||||||
|
"type": f"asset--{asset_type}",
|
||||||
|
"id": str(uuid),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rels["asset"] = {"data": assets}
|
||||||
|
if "quantities" in self.fields:
|
||||||
|
quantities = []
|
||||||
|
for uuid in source_data["quantities"]:
|
||||||
|
quantities.append(
|
||||||
|
{
|
||||||
|
# TODO: support other quantity types
|
||||||
|
"type": "quantity--standard",
|
||||||
|
"id": str(uuid),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rels["quantity"] = {"data": quantities}
|
||||||
|
|
||||||
|
payload = {"attributes": attrs, "relationships": rels}
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
||||||
importers["PlantType"] = PlantTypeImporter
|
importers["PlantType"] = PlantTypeImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
importers["PlantAsset"] = PlantAssetImporter
|
||||||
importers["Unit"] = UnitImporter
|
importers["Unit"] = UnitImporter
|
||||||
|
importers["StandardQuantity"] = StandardQuantityImporter
|
||||||
importers["ActivityLog"] = ActivityLogImporter
|
importers["ActivityLog"] = ActivityLogImporter
|
||||||
importers["HarvestLog"] = HarvestLogImporter
|
importers["HarvestLog"] = HarvestLogImporter
|
||||||
importers["MedicalLog"] = MedicalLogImporter
|
importers["MedicalLog"] = MedicalLogImporter
|
||||||
|
|
@ -347,6 +348,49 @@ class StructureAssetImporter(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# quantity importers
|
||||||
|
##############################
|
||||||
|
|
||||||
|
|
||||||
|
class FromWuttaFarmQuantity(FromWuttaFarm):
|
||||||
|
"""
|
||||||
|
Base class for WuttaFarm -> farmOS quantity importers
|
||||||
|
"""
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"measure",
|
||||||
|
"value_numerator",
|
||||||
|
"value_denominator",
|
||||||
|
"label",
|
||||||
|
"quantity_type_uuid",
|
||||||
|
"unit_uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_source_object(self, qty):
|
||||||
|
return {
|
||||||
|
"uuid": qty.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"measure": qty.measure_id,
|
||||||
|
"value_numerator": qty.value_numerator,
|
||||||
|
"value_denominator": qty.value_denominator,
|
||||||
|
"label": qty.label,
|
||||||
|
"quantity_type_uuid": qty.quantity_type.farmos_uuid,
|
||||||
|
"unit_uuid": qty.units.farmos_uuid,
|
||||||
|
"_src_object": qty,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StandardQuantityImporter(
|
||||||
|
FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Standard Quantities
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.StandardQuantity
|
||||||
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# log importers
|
# log importers
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -365,6 +409,9 @@ class FromWuttaFarmLog(FromWuttaFarm):
|
||||||
"is_group_assignment",
|
"is_group_assignment",
|
||||||
"status",
|
"status",
|
||||||
"notes",
|
"notes",
|
||||||
|
"quick",
|
||||||
|
"assets",
|
||||||
|
"quantities",
|
||||||
]
|
]
|
||||||
|
|
||||||
def normalize_source_object(self, log):
|
def normalize_source_object(self, log):
|
||||||
|
|
@ -376,6 +423,9 @@ class FromWuttaFarmLog(FromWuttaFarm):
|
||||||
"is_group_assignment": log.is_group_assignment,
|
"is_group_assignment": log.is_group_assignment,
|
||||||
"status": log.status,
|
"status": log.status,
|
||||||
"notes": log.notes,
|
"notes": log.notes,
|
||||||
|
"quick": self.config.parse_list(log.quick) if log.quick else [],
|
||||||
|
"assets": [(a.asset_type, a.farmos_uuid) for a in log.assets],
|
||||||
|
"quantities": [qty.farmos_uuid for qty in log.quantities],
|
||||||
"_src_object": log,
|
"_src_object": log,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,31 @@ class LogQuick(WuttaSet):
|
||||||
return LogQuickWidget(**kwargs)
|
return LogQuickWidget(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LogRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Custom schema type for a
|
||||||
|
:class:`~wuttafarm.db.model.log.Log` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
return model.Log
|
||||||
|
|
||||||
|
def sort_query(self, query): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
return query.order_by(self.model_class.message)
|
||||||
|
|
||||||
|
def get_object_url(self, obj): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
log = obj
|
||||||
|
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
||||||
|
|
||||||
|
|
||||||
class FarmOSUnitRef(colander.SchemaType):
|
class FarmOSUnitRef(colander.SchemaType):
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
def serialize(self, node, appstruct):
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
||||||
|
from wuttafarm.web.grids import ResourceData
|
||||||
|
|
||||||
|
|
||||||
class QuantityTypeView(FarmOSMasterView):
|
class QuantityTypeView(FarmOSMasterView):
|
||||||
|
|
@ -130,13 +131,15 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
farmos_quantity_type = None
|
farmos_quantity_type = None
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
"drupal_id",
|
||||||
|
"as_text",
|
||||||
"measure",
|
"measure",
|
||||||
"value",
|
"value",
|
||||||
|
"unit",
|
||||||
"label",
|
"label",
|
||||||
"changed",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = ("changed", "desc")
|
sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"measure",
|
"measure",
|
||||||
|
|
@ -147,20 +150,58 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
"changed",
|
"changed",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_grid_data(self, columns=None, session=None):
|
def get_farmos_api_includes(self):
|
||||||
result = self.farmos_client.resource.get("quantity", self.farmos_quantity_type)
|
return {"units"}
|
||||||
return [self.normalize_quantity(t) for t in result["data"]]
|
|
||||||
|
def get_grid_data(self, **kwargs):
|
||||||
|
return ResourceData(
|
||||||
|
self.config,
|
||||||
|
self.farmos_client,
|
||||||
|
f"quantity--{self.farmos_quantity_type}",
|
||||||
|
include=",".join(self.get_farmos_api_includes()),
|
||||||
|
normalizer=self.normalize_quantity,
|
||||||
|
)
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# drupal_id
|
||||||
|
g.set_label("drupal_id", "ID", column_only=True)
|
||||||
|
|
||||||
|
# as_text
|
||||||
|
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||||
|
|
||||||
|
# measure
|
||||||
|
g.set_renderer("measure", self.render_measure_for_grid)
|
||||||
|
|
||||||
# value
|
# value
|
||||||
g.set_link("value")
|
g.set_renderer("value", self.render_value_for_grid)
|
||||||
|
|
||||||
|
# unit
|
||||||
|
g.set_renderer("unit", self.render_unit_for_grid)
|
||||||
|
|
||||||
# changed
|
# changed
|
||||||
g.set_renderer("changed", "datetime")
|
g.set_renderer("changed", "datetime")
|
||||||
|
|
||||||
|
def render_as_text_for_grid(self, qty, field, value):
|
||||||
|
measure = qty["measure"].capitalize()
|
||||||
|
value = qty["value"]["decimal"]
|
||||||
|
units = qty["unit"]["name"] if qty["unit"] else "??"
|
||||||
|
return f"( {measure} ) {value} {units}"
|
||||||
|
|
||||||
|
def render_measure_for_grid(self, qty, field, value):
|
||||||
|
return qty["measure"].capitalize()
|
||||||
|
|
||||||
|
def render_unit_for_grid(self, qty, field, value):
|
||||||
|
unit = qty[field]
|
||||||
|
if not unit:
|
||||||
|
return ""
|
||||||
|
return unit["name"]
|
||||||
|
|
||||||
|
def render_value_for_grid(self, qty, field, value):
|
||||||
|
return qty["value"]["decimal"]
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
quantity = self.farmos_client.resource.get_id(
|
quantity = self.farmos_client.resource.get_id(
|
||||||
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
|
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
|
||||||
|
|
@ -187,7 +228,7 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
def get_instance_title(self, quantity):
|
def get_instance_title(self, quantity):
|
||||||
return quantity["value"]
|
return quantity["value"]
|
||||||
|
|
||||||
def normalize_quantity(self, quantity):
|
def normalize_quantity(self, quantity, included={}):
|
||||||
|
|
||||||
if created := quantity["attributes"]["created"]:
|
if created := quantity["attributes"]["created"]:
|
||||||
created = datetime.datetime.fromisoformat(created)
|
created = datetime.datetime.fromisoformat(created)
|
||||||
|
|
@ -197,11 +238,37 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
changed = datetime.datetime.fromisoformat(changed)
|
changed = datetime.datetime.fromisoformat(changed)
|
||||||
changed = self.app.localtime(changed)
|
changed = self.app.localtime(changed)
|
||||||
|
|
||||||
|
quantity_type_object = None
|
||||||
|
quantity_type_uuid = None
|
||||||
|
unit_object = None
|
||||||
|
unit_uuid = None
|
||||||
|
if relationships := quantity["relationships"]:
|
||||||
|
|
||||||
|
if quantity_type := relationships["quantity_type"]["data"]:
|
||||||
|
quantity_type_uuid = quantity_type["id"]
|
||||||
|
quantity_type_object = {
|
||||||
|
"uuid": quantity_type_uuid,
|
||||||
|
"type": "quantity_type--quantity_type",
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit := relationships["units"]["data"]:
|
||||||
|
unit_uuid = unit["id"]
|
||||||
|
if unit := included.get(unit_uuid):
|
||||||
|
unit_object = {
|
||||||
|
"uuid": unit_uuid,
|
||||||
|
"type": "taxonomy_term--unit",
|
||||||
|
"name": unit["attributes"]["name"],
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": quantity["id"],
|
"uuid": quantity["id"],
|
||||||
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
||||||
|
"quantity_type": quantity_type_object,
|
||||||
|
"quantity_type_uuid": quantity_type_uuid,
|
||||||
"measure": quantity["attributes"]["measure"],
|
"measure": quantity["attributes"]["measure"],
|
||||||
"value": quantity["attributes"]["value"],
|
"value": quantity["attributes"]["value"],
|
||||||
|
"unit": unit_object,
|
||||||
|
"unit_uuid": unit_uuid,
|
||||||
"label": quantity["attributes"]["label"] or colander.null,
|
"label": quantity["attributes"]["label"] or colander.null,
|
||||||
"created": created,
|
"created": created,
|
||||||
"changed": changed,
|
"changed": changed,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
|
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
|
||||||
from wuttafarm.web.forms.schema import UnitRef
|
from wuttafarm.web.forms.schema import UnitRef, LogRef
|
||||||
|
|
||||||
|
|
||||||
def get_quantity_type_enum(config):
|
def get_quantity_type_enum(config):
|
||||||
|
|
@ -119,6 +119,7 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
"value",
|
"value",
|
||||||
"units",
|
"units",
|
||||||
"label",
|
"label",
|
||||||
|
"log",
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
"farmos_uuid",
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
@ -231,6 +232,13 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
# TODO: ugh
|
# TODO: ugh
|
||||||
f.set_default("units", quantity.quantity.units)
|
f.set_default("units", quantity.quantity.units)
|
||||||
|
|
||||||
|
# log
|
||||||
|
if self.creating or self.editing:
|
||||||
|
f.remove("log")
|
||||||
|
else:
|
||||||
|
f.set_node("log", LogRef(self.request))
|
||||||
|
f.set_default("log", quantity.log)
|
||||||
|
|
||||||
def get_xref_buttons(self, quantity):
|
def get_xref_buttons(self, quantity):
|
||||||
buttons = super().get_xref_buttons(quantity)
|
buttons = super().get_xref_buttons(quantity)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import logging
|
||||||
from pyramid.renderers import render_to_response
|
from pyramid.renderers import render_to_response
|
||||||
|
|
||||||
from wuttaweb.views import View
|
from wuttaweb.views import View
|
||||||
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.util import get_farmos_client_for_user
|
from wuttafarm.web.util import get_farmos_client_for_user
|
||||||
|
|
||||||
|
|
@ -40,6 +41,8 @@ class QuickFormView(View):
|
||||||
Base class for quick form views.
|
Base class for quick form views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Session = Session
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
def __init__(self, request, context=None):
|
||||||
super().__init__(request, context=context)
|
super().__init__(request, context=context)
|
||||||
self.farmos_client = get_farmos_client_for_user(self.request)
|
self.farmos_client = get_farmos_client_for_user(self.request)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ from wuttaweb.forms.schema import WuttaDateTime
|
||||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views.quick import QuickFormView
|
from wuttafarm.web.views.quick import QuickFormView
|
||||||
from wuttafarm.web.util import get_farmos_client_for_user
|
|
||||||
|
|
||||||
|
|
||||||
class EggsQuickForm(QuickFormView):
|
class EggsQuickForm(QuickFormView):
|
||||||
|
|
@ -49,6 +48,9 @@ class EggsQuickForm(QuickFormView):
|
||||||
|
|
||||||
_layer_assets = None
|
_layer_assets = None
|
||||||
|
|
||||||
|
# TODO: make this configurable?
|
||||||
|
unit_name = "egg(s)"
|
||||||
|
|
||||||
def make_quick_form(self):
|
def make_quick_form(self):
|
||||||
f = self.make_form(
|
f = self.make_form(
|
||||||
fields=[
|
fields=[
|
||||||
|
|
@ -89,6 +91,47 @@ class EggsQuickForm(QuickFormView):
|
||||||
if self._layer_assets is not None:
|
if self._layer_assets is not None:
|
||||||
return self._layer_assets
|
return self._layer_assets
|
||||||
|
|
||||||
|
if self.app.is_farmos_wrapper():
|
||||||
|
assets = self.get_layer_assets_from_farmos()
|
||||||
|
else:
|
||||||
|
assets = self.get_layer_assets_from_wuttafarm()
|
||||||
|
|
||||||
|
assets.sort(key=lambda a: a["name"])
|
||||||
|
self._layer_assets = assets
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def get_layer_assets_from_wuttafarm(self):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
assets = []
|
||||||
|
|
||||||
|
def normalize(asset):
|
||||||
|
asset_type = asset.__wutta_hint__["farmos_asset_type"]
|
||||||
|
return {
|
||||||
|
"uuid": str(asset.farmos_uuid),
|
||||||
|
"name": asset.asset_name,
|
||||||
|
"type": f"asset--{asset_type}",
|
||||||
|
}
|
||||||
|
|
||||||
|
query = (
|
||||||
|
session.query(model.AnimalAsset)
|
||||||
|
.join(model.Asset)
|
||||||
|
.filter(model.AnimalAsset.produces_eggs == True)
|
||||||
|
.order_by(model.Asset.asset_name)
|
||||||
|
)
|
||||||
|
assets.extend([normalize(a) for a in query])
|
||||||
|
|
||||||
|
query = (
|
||||||
|
session.query(model.GroupAsset)
|
||||||
|
.join(model.Asset)
|
||||||
|
.filter(model.GroupAsset.produces_eggs == True)
|
||||||
|
.order_by(model.Asset.asset_name)
|
||||||
|
)
|
||||||
|
assets.extend([normalize(a) for a in query])
|
||||||
|
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def get_layer_assets_from_farmos(self):
|
||||||
assets = []
|
assets = []
|
||||||
params = {
|
params = {
|
||||||
"filter[produces_eggs]": 1,
|
"filter[produces_eggs]": 1,
|
||||||
|
|
@ -108,24 +151,14 @@ class EggsQuickForm(QuickFormView):
|
||||||
result = self.farmos_client.asset.get("group", params=params)
|
result = self.farmos_client.asset.get("group", params=params)
|
||||||
assets.extend([normalize(a) for a in result["data"]])
|
assets.extend([normalize(a) for a in result["data"]])
|
||||||
|
|
||||||
assets.sort(key=lambda a: a["name"])
|
|
||||||
self._layer_assets = assets
|
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
def save_quick_form(self, form):
|
def save_quick_form(self, form):
|
||||||
|
|
||||||
response = self.save_to_farmos(form)
|
if self.app.is_farmos_wrapper():
|
||||||
log = json.loads(response["create-log#body{0}"]["body"])
|
return self.save_to_farmos(form)
|
||||||
|
|
||||||
if self.app.is_farmos_mirror():
|
return self.save_to_wuttafarm(form)
|
||||||
quantity = json.loads(response["create-quantity"]["body"])
|
|
||||||
client = get_farmos_client_for_user(self.request)
|
|
||||||
self.app.auto_sync_from_farmos(
|
|
||||||
quantity["data"], "StandardQuantity", client=client
|
|
||||||
)
|
|
||||||
self.app.auto_sync_from_farmos(log["data"], "HarvestLog", client=client)
|
|
||||||
|
|
||||||
return log
|
|
||||||
|
|
||||||
def save_to_farmos(self, form):
|
def save_to_farmos(self, form):
|
||||||
data = form.validated
|
data = form.validated
|
||||||
|
|
@ -135,7 +168,7 @@ class EggsQuickForm(QuickFormView):
|
||||||
asset = assets[data["asset"]]
|
asset = assets[data["asset"]]
|
||||||
|
|
||||||
# TODO: make this configurable?
|
# TODO: make this configurable?
|
||||||
unit_name = "egg(s)"
|
unit_name = self.unit_name
|
||||||
|
|
||||||
unit = {"data": {"type": "taxonomy_term--unit"}}
|
unit = {"data": {"type": "taxonomy_term--unit"}}
|
||||||
new_unit = None
|
new_unit = None
|
||||||
|
|
@ -234,13 +267,87 @@ class EggsQuickForm(QuickFormView):
|
||||||
blueprints.insert(0, new_unit)
|
blueprints.insert(0, new_unit)
|
||||||
blueprint = SubrequestsBlueprint.parse_obj(blueprints)
|
blueprint = SubrequestsBlueprint.parse_obj(blueprints)
|
||||||
response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
|
response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
|
||||||
return response
|
|
||||||
|
|
||||||
def redirect_after_save(self, result):
|
log = json.loads(response["create-log#body{0}"]["body"])
|
||||||
return self.redirect(
|
|
||||||
self.request.route_url(
|
if self.app.is_farmos_mirror():
|
||||||
"farmos_logs_harvest.view", uuid=result["data"]["id"]
|
if new_unit:
|
||||||
|
unit = json.loads(response["create-unit"]["body"])
|
||||||
|
self.app.auto_sync_from_farmos(
|
||||||
|
unit["data"], "Unit", client=self.farmos_client
|
||||||
|
)
|
||||||
|
quantity = json.loads(response["create-quantity"]["body"])
|
||||||
|
self.app.auto_sync_from_farmos(
|
||||||
|
quantity["data"], "StandardQuantity", client=self.farmos_client
|
||||||
)
|
)
|
||||||
|
self.app.auto_sync_from_farmos(
|
||||||
|
log["data"], "HarvestLog", client=self.farmos_client
|
||||||
|
)
|
||||||
|
|
||||||
|
return log
|
||||||
|
|
||||||
|
def save_to_wuttafarm(self, form):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
data = form.validated
|
||||||
|
|
||||||
|
asset = (
|
||||||
|
session.query(model.Asset)
|
||||||
|
.filter(model.Asset.farmos_uuid == data["asset"])
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: make this configurable?
|
||||||
|
unit_name = self.unit_name
|
||||||
|
|
||||||
|
new_unit = False
|
||||||
|
unit = session.query(model.Unit).filter(model.Unit.name == unit_name).first()
|
||||||
|
if not unit:
|
||||||
|
unit = model.Unit(name=unit_name)
|
||||||
|
session.add(unit)
|
||||||
|
new_unit = True
|
||||||
|
|
||||||
|
quantity = model.StandardQuantity(
|
||||||
|
quantity_type_id="standard",
|
||||||
|
measure_id="count",
|
||||||
|
value_numerator=data["count"],
|
||||||
|
value_denominator=1,
|
||||||
|
units=unit,
|
||||||
|
)
|
||||||
|
session.add(quantity)
|
||||||
|
|
||||||
|
log = model.HarvestLog(
|
||||||
|
log_type="harvest",
|
||||||
|
message=f"Collected {data['count']} {unit_name}",
|
||||||
|
timestamp=self.app.make_utc(data["timestamp"]),
|
||||||
|
notes=data["notes"] or None,
|
||||||
|
quick="eggs",
|
||||||
|
status="done",
|
||||||
|
)
|
||||||
|
session.add(log)
|
||||||
|
log.assets.append(asset)
|
||||||
|
log.quantities.append(quantity.quantity)
|
||||||
|
log.owners.append(self.request.user)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
if self.app.is_farmos_mirror():
|
||||||
|
if new_unit:
|
||||||
|
self.app.auto_sync_to_farmos(unit, client=self.farmos_client)
|
||||||
|
self.app.auto_sync_to_farmos(quantity, client=self.farmos_client)
|
||||||
|
self.app.auto_sync_to_farmos(log, client=self.farmos_client)
|
||||||
|
|
||||||
|
return log
|
||||||
|
|
||||||
|
def redirect_after_save(self, log):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
if isinstance(log, model.HarvestLog):
|
||||||
|
return self.redirect(
|
||||||
|
self.request.route_url("logs_harvest.view", uuid=log.uuid)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.redirect(
|
||||||
|
self.request.route_url("farmos_logs_harvest.view", uuid=log["data"]["id"])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Master view for Units
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import Measure, Unit
|
from wuttafarm.db.model import Measure, Unit, Quantity
|
||||||
|
|
||||||
|
|
||||||
class MeasureView(WuttaFarmMasterView):
|
class MeasureView(WuttaFarmMasterView):
|
||||||
|
|
@ -52,6 +52,26 @@ class MeasureView(WuttaFarmMasterView):
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
row_model_class = Quantity
|
||||||
|
rows_viewable = True
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
"quantity_type_id": "Quantity Type ID",
|
||||||
|
"measure_id": "Measure ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
"drupal_id",
|
||||||
|
"as_text",
|
||||||
|
"quantity_type",
|
||||||
|
"value",
|
||||||
|
"units",
|
||||||
|
"label",
|
||||||
|
]
|
||||||
|
|
||||||
|
rows_sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
@ -59,6 +79,37 @@ class MeasureView(WuttaFarmMasterView):
|
||||||
# name
|
# name
|
||||||
g.set_link("name")
|
g.set_link("name")
|
||||||
|
|
||||||
|
def get_row_grid_data(self, measure):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
return session.query(model.Quantity).filter(model.Quantity.measure == measure)
|
||||||
|
|
||||||
|
def configure_row_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_row_grid(g)
|
||||||
|
|
||||||
|
# drupal_id
|
||||||
|
g.set_label("drupal_id", "ID", column_only=True)
|
||||||
|
|
||||||
|
# as_text
|
||||||
|
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||||
|
g.set_link("as_text")
|
||||||
|
|
||||||
|
# value
|
||||||
|
g.set_renderer("value", self.render_value_for_grid)
|
||||||
|
|
||||||
|
def render_as_text_for_grid(self, quantity, field, value):
|
||||||
|
return quantity.render_as_text(self.config)
|
||||||
|
|
||||||
|
def render_value_for_grid(self, quantity, field, value):
|
||||||
|
value = quantity.value_numerator / quantity.value_denominator
|
||||||
|
return self.app.render_quantity(value)
|
||||||
|
|
||||||
|
def get_row_action_url_view(self, quantity, i):
|
||||||
|
return self.request.route_url(
|
||||||
|
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -104,6 +155,26 @@ class UnitView(WuttaFarmMasterView):
|
||||||
"farmos_uuid",
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
row_model_class = Quantity
|
||||||
|
rows_viewable = True
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
"quantity_type_id": "Quantity Type ID",
|
||||||
|
"measure_id": "Measure ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
"drupal_id",
|
||||||
|
"as_text",
|
||||||
|
"quantity_type",
|
||||||
|
"measure",
|
||||||
|
"value",
|
||||||
|
"label",
|
||||||
|
]
|
||||||
|
|
||||||
|
rows_sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
@ -131,6 +202,37 @@ class UnitView(WuttaFarmMasterView):
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
def get_row_grid_data(self, unit):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
return session.query(model.Quantity).filter(model.Quantity.units == unit)
|
||||||
|
|
||||||
|
def configure_row_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_row_grid(g)
|
||||||
|
|
||||||
|
# drupal_id
|
||||||
|
g.set_label("drupal_id", "ID", column_only=True)
|
||||||
|
|
||||||
|
# as_text
|
||||||
|
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||||
|
g.set_link("as_text")
|
||||||
|
|
||||||
|
# value
|
||||||
|
g.set_renderer("value", self.render_value_for_grid)
|
||||||
|
|
||||||
|
def render_as_text_for_grid(self, quantity, field, value):
|
||||||
|
return quantity.render_as_text(self.config)
|
||||||
|
|
||||||
|
def render_value_for_grid(self, quantity, field, value):
|
||||||
|
value = quantity.value_numerator / quantity.value_denominator
|
||||||
|
return self.app.render_quantity(value)
|
||||||
|
|
||||||
|
def get_row_action_url_view(self, quantity, i):
|
||||||
|
return self.request.route_url(
|
||||||
|
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue