feat: add Standard Quantities table, views, import

This commit is contained in:
Lance Edgar 2026-02-19 17:48:33 -06:00
parent c93660ec4a
commit cfe2e4b7b4
13 changed files with 1225 additions and 131 deletions

View file

@ -0,0 +1,293 @@
"""add Standard Quantities
Revision ID: 5b6c87d8cddf
Revises: 1f98d27cabeb
Create Date: 2026-02-19 15:42:19.691148
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "5b6c87d8cddf"
down_revision: Union[str, None] = "1f98d27cabeb"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# measure
op.create_table(
"measure",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("name", sa.String(length=100), nullable=False),
sa.Column("drupal_id", sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_measure")),
sa.UniqueConstraint("drupal_id", name=op.f("uq_measure_drupal_id")),
sa.UniqueConstraint("name", name=op.f("uq_measure_name")),
)
op.create_table(
"measure_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(
"drupal_id", sa.String(length=20), 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_measure_version")
),
)
op.create_index(
op.f("ix_measure_version_end_transaction_id"),
"measure_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_measure_version_operation_type"),
"measure_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_measure_version_pk_transaction_id",
"measure_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_measure_version_pk_validity",
"measure_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_measure_version_transaction_id"),
"measure_version",
["transaction_id"],
unique=False,
)
# quantity
op.create_table(
"quantity",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("quantity_type_id", sa.String(length=50), nullable=False),
sa.Column("measure_id", sa.String(length=20), nullable=False),
sa.Column("value_numerator", sa.Integer(), nullable=False),
sa.Column("value_denominator", sa.Integer(), nullable=False),
sa.Column("units_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("label", 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.ForeignKeyConstraint(
["measure_id"],
["measure.drupal_id"],
name=op.f("fk_quantity_measure_id_measure"),
),
sa.ForeignKeyConstraint(
["quantity_type_id"],
["quantity_type.drupal_id"],
name=op.f("fk_quantity_quantity_type_id_quantity_type"),
),
sa.ForeignKeyConstraint(
["units_uuid"], ["unit.uuid"], name=op.f("fk_quantity_units_uuid_unit")
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity")),
sa.UniqueConstraint("drupal_id", name=op.f("uq_quantity_drupal_id")),
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_quantity_farmos_uuid")),
)
op.create_table(
"quantity_version",
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
sa.Column(
"quantity_type_id", sa.String(length=50), autoincrement=False, nullable=True
),
sa.Column(
"measure_id", sa.String(length=20), autoincrement=False, nullable=True
),
sa.Column("value_numerator", sa.Integer(), autoincrement=False, nullable=True),
sa.Column(
"value_denominator", sa.Integer(), autoincrement=False, nullable=True
),
sa.Column(
"units_uuid",
wuttjamaican.db.util.UUID(),
autoincrement=False,
nullable=True,
),
sa.Column("label", 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_quantity_version")
),
)
op.create_index(
op.f("ix_quantity_version_end_transaction_id"),
"quantity_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_version_operation_type"),
"quantity_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_quantity_version_pk_transaction_id",
"quantity_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_quantity_version_pk_validity",
"quantity_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_version_transaction_id"),
"quantity_version",
["transaction_id"],
unique=False,
)
# quantity_standard
op.create_table(
"quantity_standard",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["uuid"], ["quantity.uuid"], name=op.f("fk_quantity_standard_uuid_quantity")
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity_standard")),
)
op.create_table(
"quantity_standard_version",
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_quantity_standard_version")
),
)
op.create_index(
op.f("ix_quantity_standard_version_end_transaction_id"),
"quantity_standard_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_standard_version_operation_type"),
"quantity_standard_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_quantity_standard_version_pk_transaction_id",
"quantity_standard_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_quantity_standard_version_pk_validity",
"quantity_standard_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_standard_version_transaction_id"),
"quantity_standard_version",
["transaction_id"],
unique=False,
)
def downgrade() -> None:
# quantity_standard
op.drop_index(
op.f("ix_quantity_standard_version_transaction_id"),
table_name="quantity_standard_version",
)
op.drop_index(
"ix_quantity_standard_version_pk_validity",
table_name="quantity_standard_version",
)
op.drop_index(
"ix_quantity_standard_version_pk_transaction_id",
table_name="quantity_standard_version",
)
op.drop_index(
op.f("ix_quantity_standard_version_operation_type"),
table_name="quantity_standard_version",
)
op.drop_index(
op.f("ix_quantity_standard_version_end_transaction_id"),
table_name="quantity_standard_version",
)
op.drop_table("quantity_standard_version")
op.drop_table("quantity_standard")
# quantity
op.drop_index(
op.f("ix_quantity_version_transaction_id"), table_name="quantity_version"
)
op.drop_index("ix_quantity_version_pk_validity", table_name="quantity_version")
op.drop_index(
"ix_quantity_version_pk_transaction_id", table_name="quantity_version"
)
op.drop_index(
op.f("ix_quantity_version_operation_type"), table_name="quantity_version"
)
op.drop_index(
op.f("ix_quantity_version_end_transaction_id"), table_name="quantity_version"
)
op.drop_table("quantity_version")
op.drop_table("quantity")
# measure
op.drop_index(
op.f("ix_measure_version_transaction_id"), table_name="measure_version"
)
op.drop_index("ix_measure_version_pk_validity", table_name="measure_version")
op.drop_index("ix_measure_version_pk_transaction_id", table_name="measure_version")
op.drop_index(
op.f("ix_measure_version_operation_type"), table_name="measure_version"
)
op.drop_index(
op.f("ix_measure_version_end_transaction_id"), table_name="measure_version"
)
op.drop_table("measure_version")
op.drop_table("measure")

View file

@ -30,8 +30,8 @@ from wuttjamaican.db.model import *
from .users import WuttaFarmUser from .users import WuttaFarmUser
# wuttafarm proper models # wuttafarm proper models
from .unit import Unit from .unit import Unit, Measure
from .quantities import QuantityType from .quantities import QuantityType, Quantity, StandardQuantity
from .asset import AssetType, Asset, AssetParent from .asset import AssetType, Asset, AssetParent
from .asset_land import LandType, LandAsset from .asset_land import LandType, LandAsset
from .asset_structure import StructureType, StructureAsset from .asset_structure import StructureType, StructureAsset

View file

@ -24,6 +24,8 @@ Model definition for Quantities
""" """
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr
from wuttjamaican.db import model from wuttjamaican.db import model
@ -79,3 +81,141 @@ class QuantityType(model.Base):
def __str__(self): def __str__(self):
return self.name or "" return self.name or ""
class Quantity(model.Base):
"""
Represents a base quantity record from farmOS
"""
__tablename__ = "quantity"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Quantity",
"model_title_plural": "All Quantities",
}
uuid = model.uuid_column()
quantity_type_id = sa.Column(
sa.String(length=50),
sa.ForeignKey("quantity_type.drupal_id"),
nullable=False,
)
quantity_type = orm.relationship(QuantityType)
measure_id = sa.Column(
sa.String(length=20),
sa.ForeignKey("measure.drupal_id"),
nullable=False,
doc="""
Measure for the quantity.
""",
)
measure = orm.relationship("Measure")
value_numerator = sa.Column(
sa.Integer(),
nullable=False,
doc="""
Numerator for the quantity value.
""",
)
value_denominator = sa.Column(
sa.Integer(),
nullable=False,
doc="""
Denominator for the quantity value.
""",
)
units_uuid = model.uuid_fk_column("unit.uuid", nullable=False)
units = orm.relationship("Unit")
label = sa.Column(
sa.String(length=255),
nullable=True,
doc="""
Optional label for the quantity.
""",
)
farmos_uuid = sa.Column(
model.UUID(),
nullable=True,
unique=True,
doc="""
UUID for the quantity within farmOS.
""",
)
drupal_id = sa.Column(
sa.Integer(),
nullable=True,
unique=True,
doc="""
Drupal internal ID for the quantity.
""",
)
def render_as_text(self, config=None):
measure = str(self.measure or self.measure_id or "")
value = self.value_numerator / self.value_denominator
if config:
app = config.get_app()
value = app.render_quantity(value)
units = str(self.units or "")
return f"( {measure} ) {value} {units}"
def __str__(self):
return self.render_as_text()
class QuantityMixin:
uuid = model.uuid_fk_column("quantity.uuid", nullable=False, primary_key=True)
@declared_attr
def quantity(cls):
return orm.relationship(Quantity)
def render_as_text(self, config=None):
return self.quantity.render_as_text(config)
def __str__(self):
return self.render_as_text()
def add_quantity_proxies(subclass):
Quantity.make_proxy(subclass, "quantity", "farmos_uuid")
Quantity.make_proxy(subclass, "quantity", "drupal_id")
Quantity.make_proxy(subclass, "quantity", "quantity_type")
Quantity.make_proxy(subclass, "quantity", "quantity_type_id")
Quantity.make_proxy(subclass, "quantity", "measure")
Quantity.make_proxy(subclass, "quantity", "measure_id")
Quantity.make_proxy(subclass, "quantity", "value_numerator")
Quantity.make_proxy(subclass, "quantity", "value_denominator")
Quantity.make_proxy(subclass, "quantity", "value_decimal")
Quantity.make_proxy(subclass, "quantity", "units_uuid")
Quantity.make_proxy(subclass, "quantity", "units")
Quantity.make_proxy(subclass, "quantity", "label")
class StandardQuantity(QuantityMixin, model.Base):
"""
Represents a Standard Quantity from farmOS
"""
__tablename__ = "quantity_standard"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Standard Quantity",
"model_title_plural": "Standard Quantities",
"farmos_quantity_type": "standard",
}
add_quantity_proxies(StandardQuantity)

View file

@ -28,6 +28,42 @@ import sqlalchemy as sa
from wuttjamaican.db import model from wuttjamaican.db import model
class Measure(model.Base):
"""
Represents a "measure" option (for quantities) from farmOS
"""
__tablename__ = "measure"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Measure",
"model_title_plural": "Measures",
}
uuid = model.uuid_column()
name = sa.Column(
sa.String(length=100),
nullable=False,
unique=True,
doc="""
Name of the measure.
""",
)
drupal_id = sa.Column(
sa.String(length=20),
nullable=True,
unique=True,
doc="""
Drupal internal ID for the measure.
""",
)
def __str__(self):
return self.name or ""
class Unit(model.Base): class Unit(model.Base):
""" """
Represents an "unit" (taxonomy term) from farmOS Represents an "unit" (taxonomy term) from farmOS

View file

@ -106,8 +106,10 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
importers["GroupAsset"] = GroupAssetImporter importers["GroupAsset"] = GroupAssetImporter
importers["PlantType"] = PlantTypeImporter importers["PlantType"] = PlantTypeImporter
importers["PlantAsset"] = PlantAssetImporter importers["PlantAsset"] = PlantAssetImporter
importers["Measure"] = MeasureImporter
importers["Unit"] = UnitImporter importers["Unit"] = UnitImporter
importers["QuantityType"] = QuantityTypeImporter importers["QuantityType"] = QuantityTypeImporter
importers["StandardQuantity"] = StandardQuantityImporter
importers["LogType"] = LogTypeImporter importers["LogType"] = LogTypeImporter
importers["ActivityLog"] = ActivityLogImporter importers["ActivityLog"] = ActivityLogImporter
importers["HarvestLog"] = HarvestLogImporter importers["HarvestLog"] = HarvestLogImporter
@ -823,6 +825,37 @@ class UserImporter(FromFarmOS, ToWutta):
############################## ##############################
class MeasureImporter(FromFarmOS, ToWutta):
"""
farmOS API WuttaFarm importer for Measures
"""
model_class = model.Measure
key = "drupal_id"
supported_fields = [
"drupal_id",
"name",
]
def get_source_objects(self):
""" """
response = self.farmos_client.session.get(
self.app.get_farmos_url("/api/quantity/standard/resource/schema")
)
response.raise_for_status()
data = response.json()
return data["definitions"]["attributes"]["properties"]["measure"]["oneOf"]
def normalize_source_object(self, measure):
""" """
return {
"drupal_id": measure["const"],
"name": measure["title"],
}
class UnitImporter(FromFarmOS, ToWutta): class UnitImporter(FromFarmOS, ToWutta):
""" """
farmOS API WuttaFarm importer for Units farmOS API WuttaFarm importer for Units
@ -1100,3 +1133,114 @@ class ObservationLogImporter(LogImporterBase):
"status", "status",
"assets", "assets",
] ]
class QuantityImporterBase(FromFarmOS, ToWutta):
"""
Base class for farmOS API WuttaFarm quantity importers
"""
def get_farmos_quantity_type(self):
return self.model_class.__wutta_hint__["farmos_quantity_type"]
def get_simple_fields(self):
""" """
fields = list(super().get_simple_fields())
# nb. must explicitly declare proxy fields
fields.extend(
[
"farmos_uuid",
"drupal_id",
"quantity_type_id",
"measure_id",
"value_numerator",
"value_denominator",
"units_uuid",
"label",
]
)
return fields
def setup(self):
super().setup()
model = self.app.model
self.quantity_types_by_farmos_uuid = {}
for quantity_type in self.target_session.query(model.QuantityType):
if quantity_type.farmos_uuid:
self.quantity_types_by_farmos_uuid[quantity_type.farmos_uuid] = (
quantity_type
)
self.units_by_farmos_uuid = {}
for unit in self.target_session.query(model.Unit):
if unit.farmos_uuid:
self.units_by_farmos_uuid[unit.farmos_uuid] = unit
def get_source_objects(self):
""" """
quantity_type = self.get_farmos_quantity_type()
result = self.farmos_client.resource.get("quantity", quantity_type)
return result["data"]
def normalize_source_object(self, quantity):
""" """
quantity_type_id = None
units_uuid = None
if relationships := quantity.get("relationships"):
if quantity_type := relationships.get("quantity_type"):
if quantity_type["data"]:
if wf_quantity_type := self.quantity_types_by_farmos_uuid.get(
UUID(quantity_type["data"]["id"])
):
quantity_type_id = wf_quantity_type.drupal_id
if units := relationships.get("units"):
if units["data"]:
if wf_unit := self.units_by_farmos_uuid.get(
UUID(units["data"]["id"])
):
units_uuid = wf_unit.uuid
if not quantity_type_id:
log.warning(
"missing/invalid quantity_type for farmOS Quantity: %s", quantity
)
return None
if not units_uuid:
log.warning("missing/invalid units for farmOS Quantity: %s", quantity)
return None
value = quantity["attributes"]["value"]
return {
"farmos_uuid": UUID(quantity["id"]),
"drupal_id": quantity["attributes"]["drupal_internal__id"],
"quantity_type_id": quantity_type_id,
"measure_id": quantity["attributes"]["measure"],
"value_numerator": value["numerator"],
"value_denominator": value["denominator"],
"units_uuid": units_uuid,
"label": quantity["attributes"]["label"],
}
class StandardQuantityImporter(QuantityImporterBase):
"""
farmOS API WuttaFarm importer for Standard Quantities
"""
model_class = model.StandardQuantity
supported_fields = [
"farmos_uuid",
"drupal_id",
"quantity_type_id",
"measure_id",
"value_numerator",
"value_denominator",
"units_uuid",
"label",
]

View file

@ -55,6 +55,26 @@ class AnimalTypeRef(ObjectRef):
return self.request.route_url("animal_types.view", uuid=animal_type.uuid) return self.request.route_url("animal_types.view", uuid=animal_type.uuid)
class FarmOSRef(colander.SchemaType):
def __init__(self, request, route_prefix, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.route_prefix = route_prefix
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
return json.dumps(appstruct)
def widget_maker(self, **kwargs):
""" """
from wuttafarm.web.forms.widgets import FarmOSRefWidget
return FarmOSRefWidget(self.request, self.route_prefix, **kwargs)
class AnimalTypeType(colander.SchemaType): class AnimalTypeType(colander.SchemaType):
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):
@ -179,6 +199,27 @@ class StructureTypeRef(ObjectRef):
return self.request.route_url("structure_types.view", uuid=structure_type.uuid) return self.request.route_url("structure_types.view", uuid=structure_type.uuid)
class UnitRef(ObjectRef):
"""
Custom schema type for a :class:`~wuttafarm.db.model.units.Unit`
reference field.
This is a subclass of
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
"""
@property
def model_class(self):
model = self.app.model
return model.Unit
def sort_query(self, query):
return query.order_by(self.model_class.name)
def get_object_url(self, unit):
return self.request.route_url("units.view", uuid=unit.uuid)
class UsersType(colander.SchemaType): class UsersType(colander.SchemaType):
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):

View file

@ -54,6 +54,33 @@ class ImageWidget(Widget):
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
class FarmOSRefWidget(Widget):
"""
Generic widget to display "any reference field" - as a link to
view the farmOS record it references. Only used by the farmOS
direct API views.
"""
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")
obj = json.loads(cstruct)
return tags.link_to(
obj["name"],
self.request.route_url(f"{self.route_prefix}.view", uuid=obj["uuid"]),
)
return super().serialize(field, cstruct, **kw)
class AnimalTypeWidget(Widget): class AnimalTypeWidget(Widget):
""" """
Widget to display an "animal type" field. Widget to display an "animal type" field.

View file

@ -134,11 +134,27 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"perm": "logs_observation.list", "perm": "logs_observation.list",
}, },
{"type": "sep"}, {"type": "sep"},
{
"title": "All Quantities",
"route": "quantities",
"perm": "quantities.list",
},
{
"title": "Standard Quantities",
"route": "quantities_standard",
"perm": "quantities_standard.list",
},
{"type": "sep"},
{ {
"title": "Log Types", "title": "Log Types",
"route": "log_types", "route": "log_types",
"perm": "log_types.list", "perm": "log_types.list",
}, },
{
"title": "Measures",
"route": "measures",
"perm": "measures.list",
},
{ {
"title": "Quantity Types", "title": "Quantity Types",
"route": "quantity_types", "route": "quantity_types",
@ -248,6 +264,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "farmos_quantity_types", "route": "farmos_quantity_types",
"perm": "farmos_quantity_types.list", "perm": "farmos_quantity_types.list",
}, },
{
"title": "Standard Quantities",
"route": "farmos_quantities_standard",
"perm": "farmos_quantities_standard.list",
},
{ {
"title": "Units", "title": "Units",
"route": "farmos_units", "route": "farmos_units",

View file

@ -28,7 +28,7 @@ from .master import FarmOSMasterView
def includeme(config): def includeme(config):
config.include("wuttafarm.web.views.farmos.users") config.include("wuttafarm.web.views.farmos.users")
config.include("wuttafarm.web.views.farmos.quantity_types") config.include("wuttafarm.web.views.farmos.quantities")
config.include("wuttafarm.web.views.farmos.asset_types") config.include("wuttafarm.web.views.farmos.asset_types")
config.include("wuttafarm.web.views.farmos.units") config.include("wuttafarm.web.views.farmos.units")
config.include("wuttafarm.web.views.farmos.land_types") config.include("wuttafarm.web.views.farmos.land_types")

View file

@ -0,0 +1,278 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttaFarm --Web app to integrate with and extend farmOS
# Copyright © 2026 Lance Edgar
#
# This file is part of WuttaFarm.
#
# WuttaFarm is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
View for farmOS Quantity Types
"""
import datetime
import colander
from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import FarmOSRef
class QuantityTypeView(FarmOSMasterView):
"""
View for farmOS Quantity Types
"""
model_name = "farmos_quantity_type"
model_title = "farmOS Quantity Type"
model_title_plural = "farmOS Quantity Types"
route_prefix = "farmos_quantity_types"
url_prefix = "/farmOS/quantity-types"
grid_columns = [
"label",
"description",
]
sort_defaults = "label"
form_fields = [
"label",
"description",
]
def get_grid_data(self, columns=None, session=None):
result = self.farmos_client.resource.get("quantity_type")
return [self.normalize_quantity_type(t) for t in result["data"]]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# label
g.set_link("label")
g.set_searchable("label")
# description
g.set_searchable("description")
def get_instance(self):
result = self.farmos_client.resource.get_id(
"quantity_type", "quantity_type", self.request.matchdict["uuid"]
)
self.raw_json = result
return self.normalize_quantity_type(result["data"])
def get_instance_title(self, quantity_type):
return quantity_type["label"]
def normalize_quantity_type(self, quantity_type):
return {
"uuid": quantity_type["id"],
"drupal_id": quantity_type["attributes"]["drupal_internal__id"],
"label": quantity_type["attributes"]["label"],
"description": quantity_type["attributes"]["description"],
}
def configure_form(self, form):
f = form
super().configure_form(f)
# description
f.set_widget("description", "notes")
def get_xref_buttons(self, quantity_type):
model = self.app.model
session = self.Session()
buttons = []
if wf_quantity_type := (
session.query(model.QuantityType)
.filter(model.QuantityType.farmos_uuid == quantity_type["uuid"])
.first()
):
buttons.append(
self.make_button(
f"View {self.app.get_title()} record",
primary=True,
url=self.request.route_url(
"quantity_types.view", uuid=wf_quantity_type.uuid
),
icon_left="eye",
)
)
return buttons
class QuantityMasterView(FarmOSMasterView):
"""
Base class for Quantity views
"""
farmos_quantity_type = None
grid_columns = [
"measure",
"value",
"label",
"changed",
]
sort_defaults = ("changed", "desc")
form_fields = [
"measure",
"value",
"units",
"label",
"created",
"changed",
]
def get_grid_data(self, columns=None, session=None):
result = self.farmos_client.resource.get("quantity", self.farmos_quantity_type)
return [self.normalize_quantity(t) for t in result["data"]]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# value
g.set_link("value")
# changed
g.set_renderer("changed", "datetime")
def get_instance(self):
quantity = self.farmos_client.resource.get_id(
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
)
self.raw_json = quantity
data = self.normalize_quantity(quantity["data"])
if relationships := quantity["data"].get("relationships"):
# add units
if units := relationships.get("units"):
if units["data"]:
unit = self.farmos_client.resource.get_id(
"taxonomy_term", "unit", units["data"]["id"]
)
data["units"] = {
"uuid": unit["data"]["id"],
"name": unit["data"]["attributes"]["name"],
}
return data
def get_instance_title(self, quantity):
return quantity["value"]
def normalize_quantity(self, quantity):
if created := quantity["attributes"]["created"]:
created = datetime.datetime.fromisoformat(created)
created = self.app.localtime(created)
if changed := quantity["attributes"]["changed"]:
changed = datetime.datetime.fromisoformat(changed)
changed = self.app.localtime(changed)
return {
"uuid": quantity["id"],
"drupal_id": quantity["attributes"]["drupal_internal__id"],
"measure": quantity["attributes"]["measure"],
"value": quantity["attributes"]["value"],
"label": quantity["attributes"]["label"] or colander.null,
"created": created,
"changed": changed,
}
def configure_form(self, form):
f = form
super().configure_form(f)
# created
f.set_node("created", WuttaDateTime(self.request))
f.set_widget("created", WuttaDateTimeWidget(self.request))
# changed
f.set_node("changed", WuttaDateTime(self.request))
f.set_widget("changed", WuttaDateTimeWidget(self.request))
# units
f.set_node("units", FarmOSRef(self.request, "farmos_units"))
class StandardQuantityView(QuantityMasterView):
"""
View for farmOS Standard Quantities
"""
model_name = "farmos_standard_quantity"
model_title = "farmOS Standard Quantity"
model_title_plural = "farmOS Standard Quantities"
route_prefix = "farmos_quantities_standard"
url_prefix = "/farmOS/quantities/standard"
farmos_quantity_type = "standard"
def get_xref_buttons(self, standard_quantity):
model = self.app.model
session = self.Session()
buttons = []
if wf_standard_quantity := (
session.query(model.StandardQuantity)
.join(model.Quantity)
.filter(model.Quantity.farmos_uuid == standard_quantity["uuid"])
.first()
):
buttons.append(
self.make_button(
f"View {self.app.get_title()} record",
primary=True,
url=self.request.route_url(
"quantities_standard.view", uuid=wf_standard_quantity.uuid
),
icon_left="eye",
)
)
return buttons
def defaults(config, **kwargs):
base = globals()
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
QuantityTypeView.defaults(config)
StandardQuantityView = kwargs.get(
"StandardQuantityView", base["StandardQuantityView"]
)
StandardQuantityView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -1,125 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttaFarm --Web app to integrate with and extend farmOS
# Copyright © 2026 Lance Edgar
#
# This file is part of WuttaFarm.
#
# WuttaFarm is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
View for farmOS Quantity Types
"""
from wuttafarm.web.views.farmos import FarmOSMasterView
class QuantityTypeView(FarmOSMasterView):
"""
View for farmOS Quantity Types
"""
model_name = "farmos_quantity_type"
model_title = "farmOS Quantity Type"
model_title_plural = "farmOS Quantity Types"
route_prefix = "farmos_quantity_types"
url_prefix = "/farmOS/quantity-types"
grid_columns = [
"label",
"description",
]
sort_defaults = "label"
form_fields = [
"label",
"description",
]
def get_grid_data(self, columns=None, session=None):
result = self.farmos_client.resource.get("quantity_type")
return [self.normalize_quantity_type(t) for t in result["data"]]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# label
g.set_link("label")
g.set_searchable("label")
# description
g.set_searchable("description")
def get_instance(self):
result = self.farmos_client.resource.get_id(
"quantity_type", "quantity_type", self.request.matchdict["uuid"]
)
self.raw_json = result
return self.normalize_quantity_type(result["data"])
def get_instance_title(self, quantity_type):
return quantity_type["label"]
def normalize_quantity_type(self, quantity_type):
return {
"uuid": quantity_type["id"],
"drupal_id": quantity_type["attributes"]["drupal_internal__id"],
"label": quantity_type["attributes"]["label"],
"description": quantity_type["attributes"]["description"],
}
def configure_form(self, form):
f = form
super().configure_form(f)
# description
f.set_widget("description", "notes")
def get_xref_buttons(self, quantity_type):
model = self.app.model
session = self.Session()
buttons = []
if wf_quantity_type := (
session.query(model.QuantityType)
.filter(model.QuantityType.farmos_uuid == quantity_type["uuid"])
.first()
):
buttons.append(
self.make_button(
f"View {self.app.get_title()} record",
primary=True,
url=self.request.route_url(
"quantity_types.view", uuid=wf_quantity_type.uuid
),
icon_left="eye",
)
)
return buttons
def defaults(config, **kwargs):
base = globals()
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
QuantityTypeView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -23,8 +23,24 @@
Master view for Quantities Master view for Quantities
""" """
from collections import OrderedDict
from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import QuantityType from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
from wuttafarm.web.forms.schema import UnitRef
def get_quantity_type_enum(config):
app = config.get_app()
model = app.model
session = Session()
quantity_types = OrderedDict()
query = session.query(model.QuantityType).order_by(model.QuantityType.name)
for quantity_type in query:
quantity_types[quantity_type.drupal_id] = quantity_type.name
return quantity_types
class QuantityTypeView(WuttaFarmMasterView): class QuantityTypeView(WuttaFarmMasterView):
@ -79,12 +95,199 @@ class QuantityTypeView(WuttaFarmMasterView):
return buttons return buttons
class QuantityMasterView(WuttaFarmMasterView):
"""
Base class for Quantity master views
"""
grid_columns = [
"drupal_id",
"as_text",
"quantity_type",
"measure",
"value",
"units",
"label",
]
sort_defaults = ("drupal_id", "desc")
form_fields = [
"quantity_type",
"as_text",
"measure",
"value",
"units",
"label",
"farmos_uuid",
"drupal_id",
]
def get_query(self, session=None):
""" """
model = self.app.model
model_class = self.get_model_class()
session = session or self.Session()
query = session.query(model_class)
if model_class is not model.Quantity:
query = query.join(model.Quantity)
query = query.join(model.Measure).join(model.Unit)
return query
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
model = self.app.model
model_class = self.get_model_class()
# drupal_id
g.set_label("drupal_id", "ID", column_only=True)
g.set_sorter("drupal_id", model.Quantity.drupal_id)
# as_text
g.set_renderer("as_text", self.render_as_text_for_grid)
g.set_link("as_text")
# quantity_type
if model_class is not model.Quantity:
g.remove("quantity_type")
else:
g.set_enum("quantity_type", get_quantity_type_enum(self.config))
# measure
g.set_sorter("measure", model.Measure.name)
# value
g.set_renderer("value", self.render_value_for_grid)
# units
g.set_sorter("units", model.Unit.name)
# label
g.set_sorter("label", model.Quantity.label)
# view action links to final quantity record
if model_class is model.Quantity:
def quantity_url(quantity, i):
return self.request.route_url(
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
)
g.add_action("view", icon="eye", url=quantity_url)
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_instance_title(self, quantity):
return quantity.render_as_text(self.config)
def configure_form(self, form):
f = form
super().configure_form(f)
quantity = form.model_instance
# as_text
if self.creating or self.editing:
f.remove("as_text")
else:
f.set_default("as_text", quantity.render_as_text(self.config))
# quantity_type
if self.creating:
f.remove("quantity_type")
else:
f.set_readonly("quantity_type")
f.set_default("quantity_type", quantity.quantity_type.name)
# measure
if self.creating:
f.remove("measure")
else:
f.set_readonly("measure")
f.set_default("measure", quantity.measure.name)
# value
if self.creating:
f.remove("value")
else:
value = quantity.value_numerator / quantity.value_denominator
value = self.app.render_quantity(value)
f.set_default(
"value",
f"{value} ({quantity.value_numerator} / {quantity.value_denominator})",
)
# units
if self.creating:
f.remove("units")
else:
f.set_readonly("units")
f.set_node("units", UnitRef(self.request))
# TODO: ugh
f.set_default("units", quantity.quantity.units)
def get_xref_buttons(self, quantity):
buttons = super().get_xref_buttons(quantity)
if quantity.farmos_uuid:
url = self.request.route_url(
f"farmos_quantities_{quantity.quantity_type_id}.view",
uuid=quantity.farmos_uuid,
)
buttons.append(
self.make_button(
"View farmOS record", primary=True, url=url, icon_left="eye"
)
)
return buttons
class QuantityView(QuantityMasterView):
"""
Master view for All Quantities
"""
model_class = Quantity
route_prefix = "quantities"
url_prefix = "/quantities"
viewable = False
creatable = False
editable = False
deletable = False
model_is_versioned = False
class StandardQuantityView(QuantityMasterView):
"""
Master view for Standard Quantities
"""
model_class = StandardQuantity
route_prefix = "quantities_standard"
url_prefix = "/quantities/standard"
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"]) QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
QuantityTypeView.defaults(config) QuantityTypeView.defaults(config)
QuantityView = kwargs.get("QuantityView", base["QuantityView"])
QuantityView.defaults(config)
StandardQuantityView = kwargs.get(
"StandardQuantityView", base["StandardQuantityView"]
)
StandardQuantityView.defaults(config)
def includeme(config): def includeme(config):
defaults(config) defaults(config)

View file

@ -24,7 +24,40 @@ Master view for Units
""" """
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Unit from wuttafarm.db.model import Measure, Unit
class MeasureView(WuttaFarmMasterView):
"""
Master view for Measures
"""
model_class = Measure
route_prefix = "measures"
url_prefix = "/measures"
grid_columns = [
"name",
"drupal_id",
]
sort_defaults = "name"
filter_defaults = {
"name": {"active": True, "verb": "contains"},
}
form_fields = [
"name",
"drupal_id",
]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# name
g.set_link("name")
class UnitView(WuttaFarmMasterView): class UnitView(WuttaFarmMasterView):
@ -34,7 +67,7 @@ class UnitView(WuttaFarmMasterView):
model_class = Unit model_class = Unit
route_prefix = "units" route_prefix = "units"
url_prefix = "/animal-types" url_prefix = "/units"
farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview" farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
@ -87,6 +120,9 @@ class UnitView(WuttaFarmMasterView):
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()
MeasureView = kwargs.get("MeasureView", base["MeasureView"])
MeasureView.defaults(config)
UnitView = kwargs.get("UnitView", base["UnitView"]) UnitView = kwargs.get("UnitView", base["UnitView"])
UnitView.defaults(config) UnitView.defaults(config)