diff --git a/CHANGELOG.md b/CHANGELOG.md index 579fc2a..f1eedfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,67 +5,6 @@ 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/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## v0.11.2 (2026-03-21) - -### Fix - -- use separate thread to sync changes to farmOS -- avoid error if asset has no geometry - -## v0.11.1 (2026-03-21) - -### Fix - -- improve behavior when deleting mirrored record from farmOS -- use correct uuid when processing webhook to delete record - -## v0.11.0 (2026-03-15) - -### Feat - -- show basic map for "fixed" assets - -### Fix - -- include LogQuantity changes when viewing Log revision - -## v0.10.0 (2026-03-11) - -### Feat - -- add support for webhooks module in farmOS - -### Fix - -- remove print statement - -## v0.9.0 (2026-03-10) - -### Feat - -- add schema, edit/sync support for Seeding Logs -- add schema, edit/sync support for Equipment Assets -- add schema, edit/sync support for Equipment Types -- add schema, edit/sync support for Water Assets -- add edit/sync support for Material Types + Material Quantities -- add edit/sync support for Material Types -- add edit/sync support for Log Quantities -- add edit/sync support for `Log.groups` -- add edit/sync support for `Log.locations` -- expose Assets field when editing a Log record -- add edit/sync support for Plant Seasons -- add edit/sync support for asset parents - -### Fix - -- avoid error when material type is unknown -- improve behavior when deleting a Standard Quantity -- cleanup grid views for All, Standard Quantities -- add ordinal for sorting Measures -- expose `is_location` and `is_fixed` for editing on Animal Asset -- allow "N/A" option for animal sex -- fix Assets column for All Logs subgrid when viewing asset - ## v0.8.0 (2026-03-04) ### Feat diff --git a/pyproject.toml b/pyproject.toml index b702d8c..1bb1dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "WuttaFarm" -version = "0.11.2" +version = "0.8.0" description = "Web app to integrate with and extend farmOS" readme = "README.md" authors = [ @@ -34,7 +34,7 @@ dependencies = [ "pyramid_exclog", "uvicorn[standard]", "WuttaSync", - "WuttaWeb[continuum]>=0.30.1", + "WuttaWeb[continuum]>=0.29.0", ] diff --git a/src/wuttafarm/app.py b/src/wuttafarm/app.py index c5a581b..cb9aed3 100644 --- a/src/wuttafarm/app.py +++ b/src/wuttafarm/app.py @@ -151,74 +151,6 @@ class WuttaFarmAppHandler(base.AppHandler): factory = self.load_object(spec) return factory(self.config, farmos_client) - def get_quantity_types(self, session=None): - """ - Returns a list of all known quantity types. - """ - model = self.model - with self.short_session(session=session) as sess: - return ( - sess.query(model.QuantityType).order_by(model.QuantityType.name).all() - ) - - def get_measures(self, session=None): - """ - Returns a list of all known measures. - """ - model = self.model - with self.short_session(session=session) as sess: - return sess.query(model.Measure).order_by(model.Measure.ordinal).all() - - def get_units(self, session=None): - """ - Returns a list of all known units. - """ - model = self.model - with self.short_session(session=session) as sess: - return sess.query(model.Unit).order_by(model.Unit.name).all() - - def get_material_types(self, session=None): - """ - Returns a list of all known material types. - """ - model = self.model - with self.short_session(session=session) as sess: - return ( - sess.query(model.MaterialType).order_by(model.MaterialType.name).all() - ) - - def get_quantity_models(self): - model = self.model - return { - "standard": model.StandardQuantity, - "material": model.MaterialQuantity, - } - - def get_true_quantity(self, quantity, require=True): - model = self.model - if not isinstance(quantity, model.Quantity): - if require and not quantity: - raise ValueError(f"quantity is not valid: {quantity}") - return quantity - - session = self.get_session(quantity) - models = self.get_quantity_models() - if require and quantity.quantity_type_id not in models: - raise ValueError( - f"quantity has invalid quantity_type_id: {quantity.quantity_type_id}" - ) - - true_quantity = session.get(models[quantity.quantity_type_id], quantity.uuid) - if require and not true_quantity: - raise ValueError(f"quantity has no true/typed quantity record: {quantity}") - - return true_quantity - - def make_true_quantity(self, quantity_type_id, **kwargs): - models = self.get_quantity_models() - kwargs["quantity_type_id"] = quantity_type_id - return models[quantity_type_id](**kwargs) - def auto_sync_to_farmos(self, obj, model_name=None, client=None, require=True): """ Export the given object to farmOS, using configured handler. @@ -270,7 +202,6 @@ class WuttaFarmAppHandler(base.AppHandler): then nothing will happen / import is silently skipped when there is no such importer. """ - model = self.app.model handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos") if model_name not in handler.importers: @@ -281,10 +212,6 @@ class WuttaFarmAppHandler(base.AppHandler): # nb. begin txn to establish the API client handler.begin_source_transaction(client) with self.short_session(commit=True) as session: - - if user := session.query(model.User).filter_by(username="farmos").first(): - session.info["continuum_user_id"] = user.uuid - handler.target_session = session importer = handler.get_importer(model_name, caches_target=False) normal = importer.normalize_source_object(obj) diff --git a/src/wuttafarm/cli/__init__.py b/src/wuttafarm/cli/__init__.py index 71d4994..cd06344 100644 --- a/src/wuttafarm/cli/__init__.py +++ b/src/wuttafarm/cli/__init__.py @@ -29,4 +29,3 @@ from .base import wuttafarm_typer from . import export_farmos from . import import_farmos from . import install -from . import process_webhooks diff --git a/src/wuttafarm/cli/process_webhooks.py b/src/wuttafarm/cli/process_webhooks.py deleted file mode 100644 index 9d66a70..0000000 --- a/src/wuttafarm/cli/process_webhooks.py +++ /dev/null @@ -1,180 +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 . -# -################################################################################ -""" -WuttaFarm CLI -""" - -import logging -import time - -import typer -from oauthlib.oauth2 import BackendApplicationClient -from requests_oauthlib import OAuth2Session - -from wuttafarm.cli import wuttafarm_typer - - -log = logging.getLogger(__name__) - - -class ChangeProcessor: - - def __init__(self, config): - self.config = config - self.app = config.get_app() - - def process_change(self, change): - if change.deleted: - self.delete_record(change) - else: - self.import_record(change) - - def import_record(self, change): - token = self.get_farmos_oauth2_token() - client = self.app.get_farmos_client(token=token) - - full_type = f"{change.entity_type}--{change.bundle}" - record = client.resource.get_id( - change.entity_type, change.bundle, change.farmos_uuid - ) - - importer_map = self.get_importer_map() - model_name = importer_map[full_type] - - self.app.auto_sync_from_farmos(record["data"], model_name, client=client) - - def delete_record(self, change): - model = self.app.model - handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos") - - importer_map = self.get_importer_map() - full_type = f"{change.entity_type}--{change.bundle}" - model_name = importer_map[full_type] - - token = self.get_farmos_oauth2_token() - client = self.app.get_farmos_client(token=token) - - # nb. begin txn to establish the API client - handler.begin_source_transaction(client) - with self.app.short_session(commit=True) as session: - handler.target_session = session - importer = handler.get_importer(model_name, caches_target=False) - - # try to attribute change to 'farmos' user - if user := session.query(model.User).filter_by(username="farmos").first(): - session.info["continuum_user_id"] = user.uuid - - # only support importers with farmos_uuid as key - # (pretty sure that covers us..can revise if needed) - if importer.get_keys() != ["farmos_uuid"]: - log.warning( - "unsupported keys for %s importer: %s", - model_name, - importer.get_keys(), - ) - return - - # delete corresponding record from our app - if obj := importer.get_target_object((change.farmos_uuid,)): - importer.delete_target_object(obj) - - # TODO: this should live elsewhere - def get_farmos_oauth2_token(self): - - client_id = self.config.get( - "farmos.oauth2.importing.client_id", default="wuttafarm" - ) - client_secret = self.config.require("farmos.oauth2.importing.client_secret") - scope = self.config.get("farmos.oauth2.importing.scope", default="farm_manager") - - client = BackendApplicationClient(client_id=client_id) - oauth = OAuth2Session(client=client) - - return oauth.fetch_token( - token_url=self.app.get_farmos_url("/oauth/token"), - include_client_id=True, - client_secret=client_secret, - scope=scope, - ) - - # TODO: this should live elsewhere - def get_importer_map(self): - return { - "asset--animal": "AnimalAsset", - "asset--equipment": "EquipmentAsset", - "asset--group": "GroupAsset", - "asset--land": "LandAsset", - "asset--plant": "PlantAsset", - "asset--structure": "StructureAsset", - "asset--water": "WaterAsset", - "log--activity": "ActivityLog", - "log--harvest": "HarvestLog", - "log--medical": "MedicalLog", - "log--observation": "ObservationLog", - "log--seeding": "SeedingLog", - "quantity--material": "MaterialQuantity", - "quantity--standard": "StandardQuantity", - "taxonomy_term--animal_type": "AnimalType", - "taxonomy_term--equipment_type": "EquipmentType", - "taxonomy_term--plant_type": "PlantType", - "taxonomy_term--season": "Season", - "taxonomy_term--material_type": "MaterialType", - "taxonomy_term--unit": "Unit", - } - - -@wuttafarm_typer.command() -def process_webhooks( - ctx: typer.Context, -): - """ - Process incoming webhook requests from farmOS. - """ - config = ctx.parent.wutta_config - app = config.get_app() - model = app.model - processor = ChangeProcessor(config) - - while True: - with app.short_session(commit=True) as session: - - query = session.query(model.WebhookChange).order_by( - model.WebhookChange.received - ) - - # nb. fetch (at most) 2 changes instead of just 1; - # this will control time delay behavior below - if changes := query[:2]: - - # process first change - change = changes[0] - log.info("processing webhook change: %s", change) - processor.process_change(change) - session.delete(change) - - # minimal time delay if 2nd change exists - if len(changes) == 2: - time.sleep(0.1) - continue - - # nothing in queue, so wait 1 sec before checking again - time.sleep(1) diff --git a/src/wuttafarm/db/alembic/versions/1c89f3fbb521_add_materialtype.py b/src/wuttafarm/db/alembic/versions/1c89f3fbb521_add_materialtype.py deleted file mode 100644 index 0d4c0f5..0000000 --- a/src/wuttafarm/db/alembic/versions/1c89f3fbb521_add_materialtype.py +++ /dev/null @@ -1,116 +0,0 @@ -"""add MaterialType - -Revision ID: 1c89f3fbb521 -Revises: 82a497e30a97 -Create Date: 2026-03-08 14:38:04.538621 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "1c89f3fbb521" -down_revision: Union[str, None] = "82a497e30a97" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # material_type - op.create_table( - "material_type", - 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_material_type")), - sa.UniqueConstraint("drupal_id", name=op.f("uq_material_type_drupal_id")), - sa.UniqueConstraint("farmos_uuid", name=op.f("uq_material_type_farmos_uuid")), - ) - op.create_table( - "material_type_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_material_type_version") - ), - ) - op.create_index( - op.f("ix_material_type_version_end_transaction_id"), - "material_type_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_material_type_version_operation_type"), - "material_type_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_material_type_version_pk_transaction_id", - "material_type_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_material_type_version_pk_validity", - "material_type_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_material_type_version_transaction_id"), - "material_type_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # material_type - op.drop_index( - op.f("ix_material_type_version_transaction_id"), - table_name="material_type_version", - ) - op.drop_index( - "ix_material_type_version_pk_validity", table_name="material_type_version" - ) - op.drop_index( - "ix_material_type_version_pk_transaction_id", table_name="material_type_version" - ) - op.drop_index( - op.f("ix_material_type_version_operation_type"), - table_name="material_type_version", - ) - op.drop_index( - op.f("ix_material_type_version_end_transaction_id"), - table_name="material_type_version", - ) - op.drop_table("material_type_version") - op.drop_table("material_type") diff --git a/src/wuttafarm/db/alembic/versions/82a497e30a97_add_measure_ordinal.py b/src/wuttafarm/db/alembic/versions/82a497e30a97_add_measure_ordinal.py deleted file mode 100644 index 2cd8057..0000000 --- a/src/wuttafarm/db/alembic/versions/82a497e30a97_add_measure_ordinal.py +++ /dev/null @@ -1,37 +0,0 @@ -"""add Measure.ordinal - -Revision ID: 82a497e30a97 -Revises: c5183b781d34 -Create Date: 2026-03-08 13:15:36.917747 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "82a497e30a97" -down_revision: Union[str, None] = "c5183b781d34" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # measure - op.add_column("measure", sa.Column("ordinal", sa.Integer(), nullable=True)) - op.add_column( - "measure_version", - sa.Column("ordinal", sa.Integer(), autoincrement=False, nullable=True), - ) - - -def downgrade() -> None: - - # measure - op.drop_column("measure_version", "ordinal") - op.drop_column("measure", "ordinal") diff --git a/src/wuttafarm/db/alembic/versions/9c53513f8862_add_materialquantity.py b/src/wuttafarm/db/alembic/versions/9c53513f8862_add_materialquantity.py deleted file mode 100644 index 6f28989..0000000 --- a/src/wuttafarm/db/alembic/versions/9c53513f8862_add_materialquantity.py +++ /dev/null @@ -1,211 +0,0 @@ -"""add MaterialQuantity - -Revision ID: 9c53513f8862 -Revises: 1c89f3fbb521 -Create Date: 2026-03-08 18:14:05.587678 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "9c53513f8862" -down_revision: Union[str, None] = "1c89f3fbb521" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # quantity_material - op.create_table( - "quantity_material", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["uuid"], ["quantity.uuid"], name=op.f("fk_quantity_material_uuid_quantity") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity_material")), - ) - op.create_table( - "quantity_material_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_material_version") - ), - ) - op.create_index( - op.f("ix_quantity_material_version_end_transaction_id"), - "quantity_material_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_quantity_material_version_operation_type"), - "quantity_material_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_quantity_material_version_pk_transaction_id", - "quantity_material_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_quantity_material_version_pk_validity", - "quantity_material_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_quantity_material_version_transaction_id"), - "quantity_material_version", - ["transaction_id"], - unique=False, - ) - - # quantity_material_material_type - op.create_table( - "quantity_material_material_type", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("quantity_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("material_type_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["material_type_uuid"], - ["material_type.uuid"], - name=op.f( - "fk_quantity_material_material_type_material_type_uuid_material_type" - ), - ), - sa.ForeignKeyConstraint( - ["quantity_uuid"], - ["quantity_material.uuid"], - name=op.f( - "fk_quantity_material_material_type_quantity_uuid_quantity_material" - ), - ), - sa.PrimaryKeyConstraint( - "uuid", name=op.f("pk_quantity_material_material_type") - ), - ) - op.create_table( - "quantity_material_material_type_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "quantity_uuid", - wuttjamaican.db.util.UUID(), - autoincrement=False, - nullable=True, - ), - sa.Column( - "material_type_uuid", - wuttjamaican.db.util.UUID(), - autoincrement=False, - nullable=True, - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint( - "uuid", - "transaction_id", - name=op.f("pk_quantity_material_material_type_version"), - ), - ) - op.create_index( - op.f("ix_quantity_material_material_type_version_end_transaction_id"), - "quantity_material_material_type_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_quantity_material_material_type_version_operation_type"), - "quantity_material_material_type_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_quantity_material_material_type_version_pk_transaction_id", - "quantity_material_material_type_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_quantity_material_material_type_version_pk_validity", - "quantity_material_material_type_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_quantity_material_material_type_version_transaction_id"), - "quantity_material_material_type_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # quantity_material_material_type - op.drop_index( - op.f("ix_quantity_material_material_type_version_transaction_id"), - table_name="quantity_material_material_type_version", - ) - op.drop_index( - "ix_quantity_material_material_type_version_pk_validity", - table_name="quantity_material_material_type_version", - ) - op.drop_index( - "ix_quantity_material_material_type_version_pk_transaction_id", - table_name="quantity_material_material_type_version", - ) - op.drop_index( - op.f("ix_quantity_material_material_type_version_operation_type"), - table_name="quantity_material_material_type_version", - ) - op.drop_index( - op.f("ix_quantity_material_material_type_version_end_transaction_id"), - table_name="quantity_material_material_type_version", - ) - op.drop_table("quantity_material_material_type_version") - op.drop_table("quantity_material_material_type") - - # quantity_material - op.drop_index( - op.f("ix_quantity_material_version_transaction_id"), - table_name="quantity_material_version", - ) - op.drop_index( - "ix_quantity_material_version_pk_validity", - table_name="quantity_material_version", - ) - op.drop_index( - "ix_quantity_material_version_pk_transaction_id", - table_name="quantity_material_version", - ) - op.drop_index( - op.f("ix_quantity_material_version_operation_type"), - table_name="quantity_material_version", - ) - op.drop_index( - op.f("ix_quantity_material_version_end_transaction_id"), - table_name="quantity_material_version", - ) - op.drop_table("quantity_material_version") - op.drop_table("quantity_material") diff --git a/src/wuttafarm/db/alembic/versions/c5183b781d34_add_plant_seasons.py b/src/wuttafarm/db/alembic/versions/c5183b781d34_add_plant_seasons.py deleted file mode 100644 index 406ad64..0000000 --- a/src/wuttafarm/db/alembic/versions/c5183b781d34_add_plant_seasons.py +++ /dev/null @@ -1,205 +0,0 @@ -"""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") diff --git a/src/wuttafarm/db/alembic/versions/dca5b48a5562_add_seedinglog.py b/src/wuttafarm/db/alembic/versions/dca5b48a5562_add_seedinglog.py deleted file mode 100644 index 6a374b4..0000000 --- a/src/wuttafarm/db/alembic/versions/dca5b48a5562_add_seedinglog.py +++ /dev/null @@ -1,108 +0,0 @@ -"""add SeedingLog - -Revision ID: dca5b48a5562 -Revises: e9b8664e1f39 -Create Date: 2026-03-10 09:52:13.999777 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "dca5b48a5562" -down_revision: Union[str, None] = "e9b8664e1f39" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log_seeding - op.create_table( - "log_seeding", - sa.Column("source", sa.String(length=255), nullable=True), - sa.Column("purchase_date", sa.DateTime(), nullable=True), - sa.Column("lot_number", sa.String(length=255), nullable=True), - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["uuid"], ["log.uuid"], name=op.f("fk_log_seeding_uuid_log") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_seeding")), - ) - op.create_table( - "log_seeding_version", - sa.Column("source", sa.String(length=255), autoincrement=False, nullable=True), - sa.Column("purchase_date", sa.DateTime(), autoincrement=False, nullable=True), - sa.Column( - "lot_number", sa.String(length=255), autoincrement=False, nullable=True - ), - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint( - "uuid", "transaction_id", name=op.f("pk_log_seeding_version") - ), - ) - op.create_index( - op.f("ix_log_seeding_version_end_transaction_id"), - "log_seeding_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_seeding_version_operation_type"), - "log_seeding_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_log_seeding_version_pk_transaction_id", - "log_seeding_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_log_seeding_version_pk_validity", - "log_seeding_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_seeding_version_transaction_id"), - "log_seeding_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # log_seeding - op.drop_index( - op.f("ix_log_seeding_version_transaction_id"), table_name="log_seeding_version" - ) - op.drop_index( - "ix_log_seeding_version_pk_validity", table_name="log_seeding_version" - ) - op.drop_index( - "ix_log_seeding_version_pk_transaction_id", table_name="log_seeding_version" - ) - op.drop_index( - op.f("ix_log_seeding_version_operation_type"), table_name="log_seeding_version" - ) - op.drop_index( - op.f("ix_log_seeding_version_end_transaction_id"), - table_name="log_seeding_version", - ) - op.drop_table("log_seeding_version") - op.drop_table("log_seeding") diff --git a/src/wuttafarm/db/alembic/versions/dd4d4142b96d_add_webhookchange.py b/src/wuttafarm/db/alembic/versions/dd4d4142b96d_add_webhookchange.py deleted file mode 100644 index 5b566b2..0000000 --- a/src/wuttafarm/db/alembic/versions/dd4d4142b96d_add_webhookchange.py +++ /dev/null @@ -1,41 +0,0 @@ -"""add WebhookChange - -Revision ID: dd4d4142b96d -Revises: dca5b48a5562 -Create Date: 2026-03-10 22:31:54.324952 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "dd4d4142b96d" -down_revision: Union[str, None] = "dca5b48a5562" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # webhook_change - op.create_table( - "webhook_change", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("entity_type", sa.String(length=100), nullable=False), - sa.Column("bundle", sa.String(length=100), nullable=False), - sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("deleted", sa.Boolean(), nullable=False), - sa.Column("received", sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_webhook_change")), - ) - - -def downgrade() -> None: - - # webhook_change - op.drop_table("webhook_change") diff --git a/src/wuttafarm/db/alembic/versions/de1197d24485_add_waterasset.py b/src/wuttafarm/db/alembic/versions/de1197d24485_add_waterasset.py deleted file mode 100644 index e123fc3..0000000 --- a/src/wuttafarm/db/alembic/versions/de1197d24485_add_waterasset.py +++ /dev/null @@ -1,100 +0,0 @@ -"""add WaterAsset - -Revision ID: de1197d24485 -Revises: 9c53513f8862 -Create Date: 2026-03-09 14:59:12.032318 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "de1197d24485" -down_revision: Union[str, None] = "9c53513f8862" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # asset_water - op.create_table( - "asset_water", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["uuid"], ["asset.uuid"], name=op.f("fk_asset_water_uuid_asset") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_water")), - ) - op.create_table( - "asset_water_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_asset_water_version") - ), - ) - op.create_index( - op.f("ix_asset_water_version_end_transaction_id"), - "asset_water_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_water_version_operation_type"), - "asset_water_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_asset_water_version_pk_transaction_id", - "asset_water_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_asset_water_version_pk_validity", - "asset_water_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_water_version_transaction_id"), - "asset_water_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # asset_water - op.drop_index( - op.f("ix_asset_water_version_transaction_id"), table_name="asset_water_version" - ) - op.drop_index( - "ix_asset_water_version_pk_validity", table_name="asset_water_version" - ) - op.drop_index( - "ix_asset_water_version_pk_transaction_id", table_name="asset_water_version" - ) - op.drop_index( - op.f("ix_asset_water_version_operation_type"), table_name="asset_water_version" - ) - op.drop_index( - op.f("ix_asset_water_version_end_transaction_id"), - table_name="asset_water_version", - ) - op.drop_table("asset_water_version") - op.drop_table("asset_water") diff --git a/src/wuttafarm/db/alembic/versions/e5b27eac471c_add_equipmenttype.py b/src/wuttafarm/db/alembic/versions/e5b27eac471c_add_equipmenttype.py deleted file mode 100644 index a436725..0000000 --- a/src/wuttafarm/db/alembic/versions/e5b27eac471c_add_equipmenttype.py +++ /dev/null @@ -1,118 +0,0 @@ -"""add EquipmentType - -Revision ID: e5b27eac471c -Revises: de1197d24485 -Create Date: 2026-03-09 15:45:35.047694 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "e5b27eac471c" -down_revision: Union[str, None] = "de1197d24485" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # equipment_type - op.create_table( - "equipment_type", - 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("drupal_id", sa.Integer(), nullable=True), - sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_equipment_type")), - sa.UniqueConstraint("drupal_id", name=op.f("uq_equipment_type_drupal_id")), - sa.UniqueConstraint("farmos_uuid", name=op.f("uq_equipment_type_farmos_uuid")), - sa.UniqueConstraint("name", name=op.f("uq_equipment_type_name")), - ) - op.create_table( - "equipment_type_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("drupal_id", sa.Integer(), autoincrement=False, nullable=True), - sa.Column( - "farmos_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_equipment_type_version") - ), - ) - op.create_index( - op.f("ix_equipment_type_version_end_transaction_id"), - "equipment_type_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_equipment_type_version_operation_type"), - "equipment_type_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_equipment_type_version_pk_transaction_id", - "equipment_type_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_equipment_type_version_pk_validity", - "equipment_type_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_equipment_type_version_transaction_id"), - "equipment_type_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # equipment_type - op.drop_index( - op.f("ix_equipment_type_version_transaction_id"), - table_name="equipment_type_version", - ) - op.drop_index( - "ix_equipment_type_version_pk_validity", table_name="equipment_type_version" - ) - op.drop_index( - "ix_equipment_type_version_pk_transaction_id", - table_name="equipment_type_version", - ) - op.drop_index( - op.f("ix_equipment_type_version_operation_type"), - table_name="equipment_type_version", - ) - op.drop_index( - op.f("ix_equipment_type_version_end_transaction_id"), - table_name="equipment_type_version", - ) - op.drop_table("equipment_type_version") - op.drop_table("equipment_type") diff --git a/src/wuttafarm/db/alembic/versions/e9b8664e1f39_add_equipmentasset.py b/src/wuttafarm/db/alembic/versions/e9b8664e1f39_add_equipmentasset.py deleted file mode 100644 index 2a8ed15..0000000 --- a/src/wuttafarm/db/alembic/versions/e9b8664e1f39_add_equipmentasset.py +++ /dev/null @@ -1,218 +0,0 @@ -"""add EquipmentAsset - -Revision ID: e9b8664e1f39 -Revises: e5b27eac471c -Create Date: 2026-03-09 18:05:54.917562 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "e9b8664e1f39" -down_revision: Union[str, None] = "e5b27eac471c" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # asset_equipment - op.create_table( - "asset_equipment", - sa.Column("manufacturer", sa.String(length=255), nullable=True), - sa.Column("model", sa.String(length=255), nullable=True), - sa.Column("serial_number", sa.String(length=255), nullable=True), - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["uuid"], ["asset.uuid"], name=op.f("fk_asset_equipment_uuid_asset") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_equipment")), - ) - op.create_table( - "asset_equipment_version", - sa.Column( - "manufacturer", sa.String(length=255), autoincrement=False, nullable=True - ), - sa.Column("model", sa.String(length=255), autoincrement=False, nullable=True), - sa.Column( - "serial_number", sa.String(length=255), autoincrement=False, nullable=True - ), - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint( - "uuid", "transaction_id", name=op.f("pk_asset_equipment_version") - ), - ) - op.create_index( - op.f("ix_asset_equipment_version_end_transaction_id"), - "asset_equipment_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_equipment_version_operation_type"), - "asset_equipment_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_asset_equipment_version_pk_transaction_id", - "asset_equipment_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_asset_equipment_version_pk_validity", - "asset_equipment_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_equipment_version_transaction_id"), - "asset_equipment_version", - ["transaction_id"], - unique=False, - ) - - # asset_equipment_equipment_type - op.create_table( - "asset_equipment_equipment_type", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("equipment_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("equipment_type_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["equipment_asset_uuid"], - ["asset_equipment.uuid"], - name=op.f( - "fk_asset_equipment_equipment_type_equipment_asset_uuid_asset_equipment" - ), - ), - sa.ForeignKeyConstraint( - ["equipment_type_uuid"], - ["equipment_type.uuid"], - name=op.f( - "fk_asset_equipment_equipment_type_equipment_type_uuid_equipment_type" - ), - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_equipment_equipment_type")), - ) - op.create_table( - "asset_equipment_equipment_type_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "equipment_asset_uuid", - wuttjamaican.db.util.UUID(), - autoincrement=False, - nullable=True, - ), - sa.Column( - "equipment_type_uuid", - wuttjamaican.db.util.UUID(), - autoincrement=False, - nullable=True, - ), - sa.Column( - "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False - ), - sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), - sa.Column("operation_type", sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint( - "uuid", - "transaction_id", - name=op.f("pk_asset_equipment_equipment_type_version"), - ), - ) - op.create_index( - op.f("ix_asset_equipment_equipment_type_version_end_transaction_id"), - "asset_equipment_equipment_type_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_equipment_equipment_type_version_operation_type"), - "asset_equipment_equipment_type_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_asset_equipment_equipment_type_version_pk_transaction_id", - "asset_equipment_equipment_type_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_asset_equipment_equipment_type_version_pk_validity", - "asset_equipment_equipment_type_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_equipment_equipment_type_version_transaction_id"), - "asset_equipment_equipment_type_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # asset_equipment_equipment_type - op.drop_index( - op.f("ix_asset_equipment_equipment_type_version_transaction_id"), - table_name="asset_equipment_equipment_type_version", - ) - op.drop_index( - "ix_asset_equipment_equipment_type_version_pk_validity", - table_name="asset_equipment_equipment_type_version", - ) - op.drop_index( - "ix_asset_equipment_equipment_type_version_pk_transaction_id", - table_name="asset_equipment_equipment_type_version", - ) - op.drop_index( - op.f("ix_asset_equipment_equipment_type_version_operation_type"), - table_name="asset_equipment_equipment_type_version", - ) - op.drop_index( - op.f("ix_asset_equipment_equipment_type_version_end_transaction_id"), - table_name="asset_equipment_equipment_type_version", - ) - op.drop_table("asset_equipment_equipment_type_version") - op.drop_table("asset_equipment_equipment_type") - - # asset_equipment - op.drop_index( - op.f("ix_asset_equipment_version_transaction_id"), - table_name="asset_equipment_version", - ) - op.drop_index( - "ix_asset_equipment_version_pk_validity", table_name="asset_equipment_version" - ) - op.drop_index( - "ix_asset_equipment_version_pk_transaction_id", - table_name="asset_equipment_version", - ) - op.drop_index( - op.f("ix_asset_equipment_version_operation_type"), - table_name="asset_equipment_version", - ) - op.drop_index( - op.f("ix_asset_equipment_version_end_transaction_id"), - table_name="asset_equipment_version", - ) - op.drop_table("asset_equipment_version") - op.drop_table("asset_equipment") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index 716fd1c..15514fb 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -31,34 +31,15 @@ from .users import WuttaFarmUser # wuttafarm proper models from .unit import Unit, Measure -from .material_type import MaterialType -from .quantities import ( - QuantityType, - Quantity, - StandardQuantity, - MaterialQuantity, - MaterialQuantityMaterialType, -) +from .quantities import QuantityType, Quantity, StandardQuantity from .asset import AssetType, Asset, AssetParent from .asset_land import LandType, LandAsset from .asset_structure import StructureType, StructureAsset -from .asset_equipment import EquipmentType, EquipmentAsset, EquipmentAssetEquipmentType from .asset_animal import AnimalType, AnimalAsset from .asset_group import GroupAsset -from .asset_plant import ( - PlantType, - Season, - PlantAsset, - PlantAssetPlantType, - PlantAssetSeason, -) -from .asset_water import WaterAsset +from .asset_plant import PlantType, PlantAsset, PlantAssetPlantType from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner from .log_activity import ActivityLog from .log_harvest import HarvestLog from .log_medical import MedicalLog from .log_observation import ObservationLog -from .log_seeding import SeedingLog - -# misc. -from .webhook import WebhookChange diff --git a/src/wuttafarm/db/model/asset_equipment.py b/src/wuttafarm/db/model/asset_equipment.py deleted file mode 100644 index 51af9ee..0000000 --- a/src/wuttafarm/db/model/asset_equipment.py +++ /dev/null @@ -1,133 +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 . -# -################################################################################ -""" -Model definition for Equipment -""" - -import sqlalchemy as sa -from sqlalchemy import orm -from sqlalchemy.ext.associationproxy import association_proxy - -from wuttjamaican.db import model - -from wuttafarm.db.model.taxonomy import TaxonomyMixin -from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies - - -class EquipmentType(TaxonomyMixin, model.Base): - """ - Represents an "equipment type" (taxonomy term) from farmOS - """ - - __tablename__ = "equipment_type" - __versioned__ = {} - __wutta_hint__ = { - "model_title": "Equipment Type", - "model_title_plural": "Equipment Types", - } - - _equipment_assets = orm.relationship( - "EquipmentAssetEquipmentType", - cascade_backrefs=False, - back_populates="equipment_type", - ) - - -class EquipmentAsset(AssetMixin, model.Base): - """ - Represents an equipment asset from farmOS - """ - - __tablename__ = "asset_equipment" - __versioned__ = {} - __wutta_hint__ = { - "model_title": "Equipment Asset", - "model_title_plural": "Equipment Assets", - "farmos_asset_type": "equipment", - } - - manufacturer = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Name of the manufacturer, if applicable. - """, - ) - - model = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Model name for the equipment, if applicable. - """, - ) - - serial_number = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Serial number for the equipment, if applicable. - """, - ) - - _equipment_types = orm.relationship( - "EquipmentAssetEquipmentType", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="equipment_asset", - ) - - equipment_types = association_proxy( - "_equipment_types", - "equipment_type", - creator=lambda pt: EquipmentAssetEquipmentType(equipment_type=pt), - ) - - -add_asset_proxies(EquipmentAsset) - - -class EquipmentAssetEquipmentType(model.Base): - """ - Associates one or more equipment types with an equipment asset. - """ - - __tablename__ = "asset_equipment_equipment_type" - __versioned__ = {} - - uuid = model.uuid_column() - - equipment_asset_uuid = model.uuid_fk_column("asset_equipment.uuid", nullable=False) - equipment_asset = orm.relationship( - EquipmentAsset, - foreign_keys=equipment_asset_uuid, - back_populates="_equipment_types", - ) - - equipment_type_uuid = model.uuid_fk_column("equipment_type.uuid", nullable=False) - equipment_type = orm.relationship( - EquipmentType, - doc=""" - Reference to the equipment type. - """, - back_populates="_equipment_assets", - ) diff --git a/src/wuttafarm/db/model/asset_plant.py b/src/wuttafarm/db/model/asset_plant.py index fa1be03..62f7e9b 100644 --- a/src/wuttafarm/db/model/asset_plant.py +++ b/src/wuttafarm/db/model/asset_plant.py @@ -91,65 +91,6 @@ class PlantType(model.Base): 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): """ Represents a plant asset from farmOS @@ -176,19 +117,6 @@ class PlantAsset(AssetMixin, model.Base): 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) @@ -218,30 +146,3 @@ class PlantAssetPlantType(model.Base): """, 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", - ) diff --git a/src/wuttafarm/db/model/asset_water.py b/src/wuttafarm/db/model/asset_water.py deleted file mode 100644 index 046c899..0000000 --- a/src/wuttafarm/db/model/asset_water.py +++ /dev/null @@ -1,45 +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 . -# -################################################################################ -""" -Model definition for Water Assets -""" - -from wuttjamaican.db import model - -from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies - - -class WaterAsset(AssetMixin, model.Base): - """ - Represents a water asset from farmOS - """ - - __tablename__ = "asset_water" - __versioned__ = {} - __wutta_hint__ = { - "model_title": "Water Asset", - "model_title_plural": "Water Assets", - "farmos_asset_type": "water", - } - - -add_asset_proxies(WaterAsset) diff --git a/src/wuttafarm/db/model/log_seeding.py b/src/wuttafarm/db/model/log_seeding.py deleted file mode 100644 index 7f68923..0000000 --- a/src/wuttafarm/db/model/log_seeding.py +++ /dev/null @@ -1,71 +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 . -# -################################################################################ -""" -Model definition for Seeding Logs -""" - -import sqlalchemy as sa - -from wuttjamaican.db import model - -from wuttafarm.db.model.log import LogMixin, add_log_proxies - - -class SeedingLog(LogMixin, model.Base): - """ - Represents a Seeding Log from farmOS - """ - - __tablename__ = "log_seeding" - __versioned__ = {} - __wutta_hint__ = { - "model_title": "Seeding Log", - "model_title_plural": "Seeding Logs", - "farmos_log_type": "seeding", - } - - source = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Where the seed was obtained, if applicable. - """, - ) - - purchase_date = sa.Column( - sa.DateTime(), - nullable=True, - doc=""" - When the seed was purchased, if applicable. - """, - ) - - lot_number = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Lot number for the seed, if applicable. - """, - ) - - -add_log_proxies(SeedingLog) diff --git a/src/wuttafarm/db/model/material_type.py b/src/wuttafarm/db/model/material_type.py deleted file mode 100644 index a124451..0000000 --- a/src/wuttafarm/db/model/material_type.py +++ /dev/null @@ -1,100 +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 . -# -################################################################################ -""" -Model definition for Material Types -""" - -import sqlalchemy as sa -from sqlalchemy import orm -from sqlalchemy.ext.associationproxy import association_proxy - -from wuttjamaican.db import model - - -class MaterialType(model.Base): - """ - Represents a "material type" (taxonomy term) from farmOS - """ - - __tablename__ = "material_type" - __versioned__ = {} - __wutta_hint__ = { - "model_title": "Material Type", - "model_title_plural": "Material Types", - } - - uuid = model.uuid_column() - - name = sa.Column( - sa.String(length=100), - nullable=False, - doc=""" - Name of the material type. - """, - ) - - description = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Optional description for the material type. - """, - ) - - farmos_uuid = sa.Column( - model.UUID(), - nullable=True, - unique=True, - doc=""" - UUID for the material type within farmOS. - """, - ) - - drupal_id = sa.Column( - sa.Integer(), - nullable=True, - unique=True, - doc=""" - Drupal internal ID for the material type. - """, - ) - - _quantities = orm.relationship( - "MaterialQuantityMaterialType", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="material_type", - ) - - def _make_material_quantity(qty): - from wuttafarm.db.model import MaterialQuantityMaterialType - - return MaterialQuantityMaterialType(quantity=qty) - - quantities = association_proxy( - "_quantities", - "quantity", - creator=_make_material_quantity, - ) - - def __str__(self): - return self.name or "" diff --git a/src/wuttafarm/db/model/quantities.py b/src/wuttafarm/db/model/quantities.py index 4fa92af..4bed6a0 100644 --- a/src/wuttafarm/db/model/quantities.py +++ b/src/wuttafarm/db/model/quantities.py @@ -181,13 +181,9 @@ class Quantity(model.Base): creator=make_log_quantity, ) - def get_value_decimal(self): - # TODO: should actually return a decimal here? - return self.value_numerator / self.value_denominator - def render_as_text(self, config=None): measure = str(self.measure or self.measure_id or "") - value = self.get_value_decimal() + value = self.value_numerator / self.value_denominator if config: app = config.get_app() value = app.render_quantity(value) @@ -204,15 +200,7 @@ class QuantityMixin: @declared_attr def quantity(cls): - return orm.relationship( - Quantity, - single_parent=True, - cascade="all, delete-orphan", - cascade_backrefs=False, - ) - - def get_value_decimal(self): - return self.quantity.get_value_decimal() + return orm.relationship(Quantity) def render_as_text(self, config=None): return self.quantity.render_as_text(config) @@ -252,64 +240,3 @@ class StandardQuantity(QuantityMixin, model.Base): add_quantity_proxies(StandardQuantity) - - -class MaterialQuantity(QuantityMixin, model.Base): - """ - Represents a Material Quantity from farmOS - """ - - __tablename__ = "quantity_material" - __versioned__ = {} - __wutta_hint__ = { - "model_title": "Material Quantity", - "model_title_plural": "Material Quantities", - "farmos_quantity_type": "material", - } - - _material_types = orm.relationship( - "MaterialQuantityMaterialType", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="quantity", - ) - - material_types = association_proxy( - "_material_types", - "material_type", - creator=lambda mtype: MaterialQuantityMaterialType(material_type=mtype), - ) - - def render_as_text(self, config=None): - text = super().render_as_text(config) - mtypes = ", ".join([str(mt) for mt in self.material_types]) - return f"{mtypes} {text}" - - -add_quantity_proxies(MaterialQuantity) - - -class MaterialQuantityMaterialType(model.Base): - """ - Represents a "material quantity's material type relationship" from - farmOS. - """ - - __tablename__ = "quantity_material_material_type" - __versioned__ = {} - - uuid = model.uuid_column() - - quantity_uuid = model.uuid_fk_column("quantity_material.uuid", nullable=False) - quantity = orm.relationship( - MaterialQuantity, - foreign_keys=quantity_uuid, - back_populates="_material_types", - ) - - material_type_uuid = model.uuid_fk_column("material_type.uuid", nullable=False) - material_type = orm.relationship( - "MaterialType", - foreign_keys=material_type_uuid, - back_populates="_quantities", - ) diff --git a/src/wuttafarm/db/model/taxonomy.py b/src/wuttafarm/db/model/taxonomy.py deleted file mode 100644 index 3d84197..0000000 --- a/src/wuttafarm/db/model/taxonomy.py +++ /dev/null @@ -1,74 +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 . -# -################################################################################ -""" -Base logic for taxonomy term models -""" - -import sqlalchemy as sa - -from wuttjamaican.db import model - - -class TaxonomyMixin: - """ - Mixin for taxonomy term models - """ - - uuid = model.uuid_column() - - name = sa.Column( - sa.String(length=100), - nullable=False, - unique=True, - doc=""" - Name for the taxonomy term. - """, - ) - - description = sa.Column( - sa.String(length=255), - nullable=True, - doc=""" - Optional description for the taxonomy term. - """, - ) - - drupal_id = sa.Column( - sa.Integer(), - nullable=True, - unique=True, - doc=""" - Drupal internal ID for the taxonomy term. - """, - ) - - farmos_uuid = sa.Column( - model.UUID(), - nullable=True, - unique=True, - doc=""" - UUID for the taxonomy term within farmOS. - """, - ) - - def __str__(self): - return self.name or "" diff --git a/src/wuttafarm/db/model/unit.py b/src/wuttafarm/db/model/unit.py index a376e2c..e9c6e70 100644 --- a/src/wuttafarm/db/model/unit.py +++ b/src/wuttafarm/db/model/unit.py @@ -42,14 +42,6 @@ class Measure(model.Base): uuid = model.uuid_column() - ordinal = sa.Column( - sa.Integer(), - nullable=True, - doc=""" - Ordinal (sequence number) for the measure. - """, - ) - name = sa.Column( sa.String(length=100), nullable=False, diff --git a/src/wuttafarm/db/model/webhook.py b/src/wuttafarm/db/model/webhook.py deleted file mode 100644 index b96a572..0000000 --- a/src/wuttafarm/db/model/webhook.py +++ /dev/null @@ -1,61 +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 . -# -################################################################################ -""" -Model definition for webhook changes -""" - -import sqlalchemy as sa - -from wuttjamaican.db import model -from wuttjamaican.util import make_utc - - -class WebhookChange(model.Base): - """ - Represents a "change" (create/update/delete) notification which - originated in farmOS and delivered via webhook. - - This table serves as a "FIFO queue" for processing the changes. - """ - - __tablename__ = "webhook_change" - - uuid = model.uuid_column() - - entity_type = sa.Column(sa.String(length=100), nullable=False) - bundle = sa.Column(sa.String(length=100), nullable=False) - farmos_uuid = sa.Column(model.UUID(), nullable=False) - - deleted = sa.Column(sa.Boolean(), nullable=False) - - received = sa.Column( - sa.DateTime(), - nullable=False, - default=make_utc, - doc=""" - Date and time when the change was obtained from the watcher thread. - """, - ) - - def __str__(self): - event_type = "delete" if self.deleted else "create/update" - return f"{event_type} {self.entity_type}--{self.bundle}: {self.farmos_uuid}" diff --git a/src/wuttafarm/farmos/importing/model.py b/src/wuttafarm/farmos/importing/model.py index 011a170..ad1cb38 100644 --- a/src/wuttafarm/farmos/importing/model.py +++ b/src/wuttafarm/farmos/importing/model.py @@ -71,15 +71,13 @@ class ToFarmOSTaxonomy(ToFarmOS): supported_fields = [ "uuid", "name", - "description", ] def get_target_objects(self, **kwargs): - return list( - self.farmos_client.resource.iterate( - "taxonomy_term", self.farmos_taxonomy_type - ) + result = self.farmos_client.resource.get( + "taxonomy_term", self.farmos_taxonomy_type ) + return result["data"] def get_target_object(self, key): @@ -103,24 +101,17 @@ class ToFarmOSTaxonomy(ToFarmOS): return result["data"] def normalize_target_object(self, obj): - if description := obj["attributes"]["description"]: - description = description["value"] return { "uuid": UUID(obj["id"]), "name": obj["attributes"]["name"], - "description": description, } def get_term_payload(self, source_data): - - attrs = {} - if "name" in self.fields: - attrs["name"] = source_data["name"] - if "description" in self.fields: - attrs["description"] = {"value": source_data["description"]} - - payload = {"attributes": attrs} - return payload + return { + "attributes": { + "name": source_data["name"], + } + } def create_target_object(self, key, source_data): if source_data.get("__ignoreme__"): @@ -136,9 +127,9 @@ class ToFarmOSTaxonomy(ToFarmOS): normal["_new_object"] = result["data"] return normal - def update_target_object(self, term, source_data, target_data=None): + def update_target_object(self, asset, source_data, target_data=None): if self.dry_run: - return term + return asset payload = self.get_term_payload(source_data) payload["id"] = str(source_data["uuid"]) @@ -155,12 +146,9 @@ class ToFarmOSAsset(ToFarmOS): farmos_asset_type = None - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.normal = self.app.get_normalizer(self.farmos_client) - def get_target_objects(self, **kwargs): - return list(self.farmos_client.asset.iterate(self.farmos_asset_type)) + assets = self.farmos_client.asset.get(self.farmos_asset_type) + return assets["data"] def get_target_object(self, key): @@ -203,17 +191,18 @@ class ToFarmOSAsset(ToFarmOS): return self.normalize_target_object(result["data"]) def normalize_target_object(self, asset): - normal = self.normal.normalize_farmos_asset(asset) + + if notes := asset["attributes"]["notes"]: + notes = notes["value"] + return { - "uuid": UUID(normal["uuid"]), - "asset_name": normal["asset_name"], - "is_location": normal["is_location"], - "is_fixed": normal["is_fixed"], - # nb. this is only used for certain asset types - "produces_eggs": normal["produces_eggs"], - "parents": [(p["asset_type"], UUID(p["uuid"])) for p in normal["parents"]], - "notes": normal["notes"], - "archived": normal["archived"], + "uuid": UUID(asset["id"]), + "asset_name": asset["attributes"]["name"], + "is_location": asset["attributes"]["is_location"], + "is_fixed": asset["attributes"]["is_fixed"], + "produces_eggs": asset["attributes"].get("produces_eggs"), + "notes": notes, + "archived": asset["attributes"]["archived"], } def get_asset_payload(self, source_data): @@ -232,18 +221,8 @@ class ToFarmOSAsset(ToFarmOS): if "archived" in self.fields: attrs["archived"] = source_data["archived"] - rels = {} - if "parents" in self.fields: - rels["parent"] = {"data": []} - for asset_type, uuid in source_data["parents"]: - rels["parent"]["data"].append( - { - "id": str(uuid), - "type": f"asset--{asset_type}", - } - ) + payload = {"attributes": attrs} - payload = {"attributes": attrs, "relationships": rels} return payload @@ -266,8 +245,6 @@ class AnimalAssetImporter(ToFarmOSAsset): "is_sterile", "produces_eggs", "birthdate", - "is_location", - "is_fixed", "notes", "archived", ] @@ -319,80 +296,6 @@ class AnimalTypeImporter(ToFarmOSTaxonomy): farmos_taxonomy_type = "animal_type" -class EquipmentTypeImporter(ToFarmOSTaxonomy): - - model_title = "EquipmentType" - farmos_taxonomy_type = "equipment_type" - - -class EquipmentAssetImporter(ToFarmOSAsset): - - model_title = "EquipmentAsset" - farmos_asset_type = "equipment" - - supported_fields = [ - "uuid", - "asset_name", - "manufacturer", - "model", - "serial_number", - "equipment_type_uuids", - "is_location", - "is_fixed", - "notes", - "archived", - ] - - def normalize_target_object(self, equipment): - data = super().normalize_target_object(equipment) - data.update( - { - "manufacturer": equipment["attributes"]["manufacturer"], - "model": equipment["attributes"]["model"], - "serial_number": equipment["attributes"]["serial_number"], - "equipment_type_uuids": [ - UUID(etype["id"]) - for etype in equipment["relationships"]["equipment_type"]["data"] - ], - } - ) - return data - - def get_asset_payload(self, source_data): - payload = super().get_asset_payload(source_data) - - attrs = {} - if "manufacturer" in self.fields: - attrs["manufacturer"] = source_data["manufacturer"] - if "model" in self.fields: - attrs["model"] = source_data["model"] - if "serial_number" in self.fields: - attrs["serial_number"] = source_data["serial_number"] - - rels = {} - if "equipment_type_uuids" in self.fields: - rels["equipment_type"] = {"data": []} - for uuid in source_data["equipment_type_uuids"]: - rels["equipment_type"]["data"].append( - { - "id": str(uuid), - "type": "taxonomy_term--equipment_type", - } - ) - - payload["attributes"].update(attrs) - if rels: - payload.setdefault("relationships", {}).update(rels) - - return payload - - -class MaterialTypeImporter(ToFarmOSTaxonomy): - - model_title = "MaterialType" - farmos_taxonomy_type = "material_type" - - class GroupAssetImporter(ToFarmOSAsset): model_title = "GroupAsset" @@ -450,12 +353,6 @@ class PlantTypeImporter(ToFarmOSTaxonomy): farmos_taxonomy_type = "plant_type" -class SeasonImporter(ToFarmOSTaxonomy): - - model_title = "Season" - farmos_taxonomy_type = "season" - - class PlantAssetImporter(ToFarmOSAsset): model_title = "PlantAsset" @@ -465,7 +362,6 @@ class PlantAssetImporter(ToFarmOSAsset): "uuid", "asset_name", "plant_type_uuids", - "season_uuids", "notes", "archived", ] @@ -477,9 +373,6 @@ class PlantAssetImporter(ToFarmOSAsset): "plant_type_uuids": [ 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 @@ -505,15 +398,6 @@ class PlantAssetImporter(ToFarmOSAsset): "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) if rels: @@ -559,21 +443,6 @@ class StructureAssetImporter(ToFarmOSAsset): return payload -class WaterAssetImporter(ToFarmOSAsset): - - model_title = "WaterAsset" - farmos_asset_type = "water" - - supported_fields = [ - "uuid", - "asset_name", - "is_location", - "is_fixed", - "notes", - "archived", - ] - - ############################## # quantity importers ############################## @@ -700,49 +569,6 @@ class ToFarmOSQuantity(ToFarmOS): return payload -class MaterialQuantityImporter(ToFarmOSQuantity): - - model_title = "MaterialQuantity" - farmos_quantity_type = "material" - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "material_types", - ] - ) - return fields - - def normalize_target_object(self, quantity): - data = super().normalize_target_object(quantity) - - if "material_types" in self.fields: - data["material_types"] = [ - UUID(mtype["id"]) - for mtype in quantity["relationships"]["material_type"]["data"] - ] - - return data - - def get_quantity_payload(self, source_data): - payload = super().get_quantity_payload(source_data) - - rels = {} - if "material_types" in self.fields: - rels["material_type"] = {"data": []} - for uuid in source_data["material_types"]: - rels["material_type"]["data"].append( - { - "id": str(uuid), - "type": "taxonomy_term--material_type", - } - ) - - payload.setdefault("relationships", {}).update(rels) - return payload - - class StandardQuantityImporter(ToFarmOSQuantity): model_title = "StandardQuantity" @@ -771,8 +597,6 @@ class ToFarmOSLog(ToFarmOS): "notes", "quick", "assets", - "locations", - "groups", "quantities", ] @@ -781,7 +605,8 @@ class ToFarmOSLog(ToFarmOS): self.normal = self.app.get_normalizer(self.farmos_client) def get_target_objects(self, **kwargs): - return list(self.farmos_client.log.iterate(self.farmos_log_type)) + result = self.farmos_client.log.get(self.farmos_log_type) + return result["data"] def get_target_object(self, key): @@ -835,10 +660,6 @@ class ToFarmOSLog(ToFarmOS): "notes": normal["notes"], "quick": normal["quick"], "assets": [(a["asset_type"], UUID(a["uuid"])) for a in normal["assets"]], - "locations": [ - (l["asset_type"], UUID(l["uuid"])) for l in normal["locations"] - ], - "groups": [(g["asset_type"], UUID(g["uuid"])) for g in normal["groups"]], "quantities": [UUID(uuid) for uuid in normal["quantity_uuids"]], } @@ -871,26 +692,6 @@ class ToFarmOSLog(ToFarmOS): } ) rels["asset"] = {"data": assets} - if "locations" in self.fields: - locations = [] - for asset_type, uuid in source_data["locations"]: - locations.append( - { - "type": f"asset--{asset_type}", - "id": str(uuid), - } - ) - rels["location"] = {"data": locations} - if "groups" in self.fields: - groups = [] - for asset_type, uuid in source_data["groups"]: - groups.append( - { - "type": f"asset--{asset_type}", - "id": str(uuid), - } - ) - rels["group"] = {"data": groups} if "quantities" in self.fields: quantities = [] for uuid in source_data["quantities"]: @@ -955,48 +756,3 @@ class ObservationLogImporter(ToFarmOSLog): model_title = "ObservationLog" farmos_log_type = "observation" - - -class SeedingLogImporter(ToFarmOSLog): - - model_title = "SeedingLog" - farmos_log_type = "seeding" - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "source", - "purchase_date", - "lot_number", - ] - ) - return fields - - def normalize_target_object(self, log): - data = super().normalize_target_object(log) - data.update( - { - "source": log["attributes"]["source"], - "purchase_date": self.normalize_datetime( - log["attributes"]["purchase_date"] - ), - "lot_number": log["attributes"]["lot_number"], - } - ) - return data - - def get_log_payload(self, source_data): - payload = super().get_log_payload(source_data) - - attrs = {} - if "source" in self.fields: - attrs["source"] = source_data["source"] - if "purchase_date" in self.fields: - attrs["purchase_date"] = self.format_datetime(source_data["purchase_date"]) - if "lot_number" in self.fields: - attrs["lot_number"] = source_data["lot_number"] - - if attrs: - payload["attributes"].update(attrs) - return payload diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py index d60a96f..8394e4c 100644 --- a/src/wuttafarm/farmos/importing/wuttafarm.py +++ b/src/wuttafarm/farmos/importing/wuttafarm.py @@ -98,24 +98,17 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler): importers = super().define_importers() importers["LandAsset"] = LandAssetImporter importers["StructureAsset"] = StructureAssetImporter - importers["WaterAsset"] = WaterAssetImporter - importers["EquipmentType"] = EquipmentTypeImporter - importers["EquipmentAsset"] = EquipmentAssetImporter importers["AnimalType"] = AnimalTypeImporter importers["AnimalAsset"] = AnimalAssetImporter importers["GroupAsset"] = GroupAssetImporter importers["PlantType"] = PlantTypeImporter - importers["Season"] = SeasonImporter importers["PlantAsset"] = PlantAssetImporter importers["Unit"] = UnitImporter - importers["MaterialType"] = MaterialTypeImporter - importers["MaterialQuantity"] = MaterialQuantityImporter importers["StandardQuantity"] = StandardQuantityImporter importers["ActivityLog"] = ActivityLogImporter importers["HarvestLog"] = HarvestLogImporter importers["MedicalLog"] = MedicalLogImporter importers["ObservationLog"] = ObservationLogImporter - importers["SeedingLog"] = SeedingLogImporter return importers @@ -141,156 +134,60 @@ class FromWuttaFarm(FromWutta): return obj -class FromWuttaFarmAsset(FromWuttaFarm): - """ - Base class for WuttaFarm → farmOS API asset exporters - """ - - supported_fields = [ - "uuid", - "asset_name", - "is_location", - "is_fixed", - "parents", - "notes", - "archived", - ] - - def normalize_source_object(self, asset): - return { - "uuid": asset.farmos_uuid or self.app.make_true_uuid(), - "asset_name": asset.asset_name, - "is_location": asset.is_location, - "is_fixed": asset.is_fixed, - "parents": [(p.asset_type, p.farmos_uuid) for p in asset.parents], - "notes": asset.notes, - "archived": asset.archived, - "_src_object": asset, - } - - -class AnimalAssetImporter( - FromWuttaFarmAsset, farmos_importing.model.AnimalAssetImporter -): +class AnimalAssetImporter(FromWuttaFarm, farmos_importing.model.AnimalAssetImporter): """ WuttaFarm → farmOS API exporter for Animal Assets """ source_model_class = model.AnimalAsset - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "animal_type_uuid", - "sex", - "is_sterile", - "produces_eggs", - "birthdate", - ] - ) - return fields - - def normalize_source_object(self, animal): - data = super().normalize_source_object(animal) - data.update( - { - "animal_type_uuid": animal.animal_type.farmos_uuid, - "sex": animal.sex, - "is_sterile": animal.is_sterile, - "produces_eggs": animal.produces_eggs, - "birthdate": animal.birthdate, - } - ) - return data - - -class FromWuttaFarmTaxonomy(FromWuttaFarm): - """ - Base class for taxonomy term exporters - """ - supported_fields = [ "uuid", - "name", - "description", + "asset_name", + "animal_type_uuid", + "sex", + "is_sterile", + "produces_eggs", + "birthdate", + "notes", + "archived", ] - drupal_internal_id_field = "drupal_internal__tid" - - def normalize_source_object(self, term): + def normalize_source_object(self, animal): return { - "uuid": term.farmos_uuid or self.app.make_true_uuid(), - "name": term.name, - "description": term.description, - "_src_object": term, + "uuid": animal.farmos_uuid or self.app.make_true_uuid(), + "asset_name": animal.asset_name, + "animal_type_uuid": animal.animal_type.farmos_uuid, + "sex": animal.sex, + "is_sterile": animal.is_sterile, + "produces_eggs": animal.produces_eggs, + "birthdate": animal.birthdate, + "notes": animal.notes, + "archived": animal.archived, + "_src_object": animal, } -class EquipmentTypeImporter( - FromWuttaFarmTaxonomy, farmos_importing.model.EquipmentTypeImporter -): - """ - WuttaFarm → farmOS API exporter for Equipment Types - """ - - source_model_class = model.EquipmentType - - -class EquipmentAssetImporter( - FromWuttaFarmAsset, farmos_importing.model.EquipmentAssetImporter -): - """ - WuttaFarm → farmOS API exporter for Equipment Assets - """ - - source_model_class = model.EquipmentAsset - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "manufacturer", - "model", - "serial_number", - "equipment_type_uuids", - ] - ) - return fields - - def normalize_source_object(self, equipment): - data = super().normalize_source_object(equipment) - data.update( - { - "manufacturer": equipment.manufacturer, - "model": equipment.model, - "serial_number": equipment.serial_number, - "equipment_type_uuids": [ - etype.farmos_uuid for etype in equipment.equipment_types - ], - } - ) - return data - - -class AnimalTypeImporter( - FromWuttaFarmTaxonomy, farmos_importing.model.AnimalTypeImporter -): +class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporter): """ WuttaFarm → farmOS API exporter for Animal Types """ source_model_class = model.AnimalType + supported_fields = [ + "uuid", + "name", + ] -class MaterialTypeImporter( - FromWuttaFarmTaxonomy, farmos_importing.model.MaterialTypeImporter -): - """ - WuttaFarm → farmOS API exporter for Material Types - """ + drupal_internal_id_field = "drupal_internal__tid" - source_model_class = model.MaterialType + def normalize_source_object(self, animal_type): + return { + "uuid": animal_type.farmos_uuid or self.app.make_true_uuid(), + "name": animal_type.name, + "_src_object": animal_type, + } class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter): @@ -315,56 +212,60 @@ class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter): } -class GroupAssetImporter(FromWuttaFarmAsset, farmos_importing.model.GroupAssetImporter): +class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter): """ WuttaFarm → farmOS API exporter for Group Assets """ source_model_class = model.GroupAsset - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "produces_eggs", - ] - ) - return fields + supported_fields = [ + "uuid", + "asset_name", + "produces_eggs", + "notes", + "archived", + ] def normalize_source_object(self, group): - data = super().normalize_source_object(group) - data.update( - { - "produces_eggs": group.produces_eggs, - } - ) - return data + return { + "uuid": group.farmos_uuid or self.app.make_true_uuid(), + "asset_name": group.asset_name, + "produces_eggs": group.produces_eggs, + "notes": group.notes, + "archived": group.archived, + "_src_object": group, + } -class LandAssetImporter(FromWuttaFarmAsset, farmos_importing.model.LandAssetImporter): +class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter): """ WuttaFarm → farmOS API exporter for Land Assets """ source_model_class = model.LandAsset - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "land_type_id", - ] - ) - return fields + supported_fields = [ + "uuid", + "asset_name", + "land_type_id", + "is_location", + "is_fixed", + "notes", + "archived", + ] def normalize_source_object(self, land): - data = super().normalize_source_object(land) - data.update( - { - "land_type_id": land.land_type.drupal_id, - } - ) - return data + return { + "uuid": land.farmos_uuid or self.app.make_true_uuid(), + "asset_name": land.asset_name, + "land_type_id": land.land_type.drupal_id, + "is_location": land.is_location, + "is_fixed": land.is_fixed, + "notes": land.notes, + "archived": land.archived, + "_src_object": land, + } class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter): @@ -389,58 +290,34 @@ 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(FromWuttaFarm, farmos_importing.model.PlantAssetImporter): """ WuttaFarm → farmOS API exporter for Plant Assets """ source_model_class = model.PlantAsset - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "plant_type_uuids", - "season_uuids", - ] - ) - return fields + supported_fields = [ + "uuid", + "asset_name", + "plant_type_uuids", + "notes", + "archived", + ] def normalize_source_object(self, plant): - data = super().normalize_source_object(plant) - data.update( - { - "plant_type_uuids": [pt.farmos_uuid for pt in plant.plant_types], - "season_uuids": [s.farmos_uuid for s in plant.seasons], - } - ) - return data + return { + "uuid": plant.farmos_uuid or self.app.make_true_uuid(), + "asset_name": plant.asset_name, + "plant_type_uuids": [t.plant_type.farmos_uuid for t in plant._plant_types], + "notes": plant.notes, + "archived": plant.archived, + "_src_object": plant, + } class StructureAssetImporter( - FromWuttaFarmAsset, farmos_importing.model.StructureAssetImporter + FromWuttaFarm, farmos_importing.model.StructureAssetImporter ): """ WuttaFarm → farmOS API exporter for Structure Assets @@ -448,31 +325,27 @@ class StructureAssetImporter( source_model_class = model.StructureAsset - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "structure_type_id", - ] - ) - return fields + supported_fields = [ + "uuid", + "asset_name", + "structure_type_id", + "is_location", + "is_fixed", + "notes", + "archived", + ] def normalize_source_object(self, structure): - data = super().normalize_source_object(structure) - data.update( - { - "structure_type_id": structure.structure_type.drupal_id, - } - ) - return data - - -class WaterAssetImporter(FromWuttaFarmAsset, farmos_importing.model.WaterAssetImporter): - """ - WuttaFarm → farmOS API exporter for Water Assets - """ - - source_model_class = model.WaterAsset + return { + "uuid": structure.farmos_uuid or self.app.make_true_uuid(), + "asset_name": structure.asset_name, + "structure_type_id": structure.structure_type.drupal_id, + "is_location": structure.is_location, + "is_fixed": structure.is_fixed, + "notes": structure.notes, + "archived": structure.archived, + "_src_object": structure, + } ############################## @@ -508,24 +381,6 @@ class FromWuttaFarmQuantity(FromWuttaFarm): } -class MaterialQuantityImporter( - FromWuttaFarmQuantity, farmos_importing.model.MaterialQuantityImporter -): - """ - WuttaFarm → farmOS API exporter for Material Quantities - """ - - source_model_class = model.MaterialQuantity - - def normalize_source_object(self, quantity): - data = super().normalize_source_object(quantity) - - if "material_types" in self.fields: - data["material_types"] = [mt.farmos_uuid for mt in quantity.material_types] - - return data - - class StandardQuantityImporter( FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter ): @@ -556,8 +411,6 @@ class FromWuttaFarmLog(FromWuttaFarm): "notes", "quick", "assets", - "locations", - "groups", "quantities", ] @@ -572,8 +425,6 @@ class FromWuttaFarmLog(FromWuttaFarm): "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], - "locations": [(l.asset_type, l.farmos_uuid) for l in log.locations], - "groups": [(g.asset_type, g.farmos_uuid) for g in log.groups], "quantities": [qty.farmos_uuid for qty in log.quantities], "_src_object": log, } @@ -629,22 +480,3 @@ class ObservationLogImporter( """ source_model_class = model.ObservationLog - - -class SeedingLogImporter(FromWuttaFarmLog, farmos_importing.model.SeedingLogImporter): - """ - WuttaFarm → farmOS API exporter for Seeding Logs - """ - - source_model_class = model.SeedingLog - - def normalize_source_object(self, log): - data = super().normalize_source_object(log) - data.update( - { - "source": log.source, - "purchase_date": log.purchase_date, - "lot_number": log.lot_number, - } - ) - return data diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 4f5a47a..6b21090 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -106,27 +106,20 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): importers["LandAsset"] = LandAssetImporter importers["StructureType"] = StructureTypeImporter importers["StructureAsset"] = StructureAssetImporter - importers["WaterAsset"] = WaterAssetImporter - importers["EquipmentType"] = EquipmentTypeImporter - importers["EquipmentAsset"] = EquipmentAssetImporter importers["AnimalType"] = AnimalTypeImporter importers["AnimalAsset"] = AnimalAssetImporter importers["GroupAsset"] = GroupAssetImporter importers["PlantType"] = PlantTypeImporter - importers["Season"] = SeasonImporter importers["PlantAsset"] = PlantAssetImporter importers["Measure"] = MeasureImporter importers["Unit"] = UnitImporter - importers["MaterialType"] = MaterialTypeImporter importers["QuantityType"] = QuantityTypeImporter importers["StandardQuantity"] = StandardQuantityImporter - importers["MaterialQuantity"] = MaterialQuantityImporter importers["LogType"] = LogTypeImporter importers["ActivityLog"] = ActivityLogImporter importers["HarvestLog"] = HarvestLogImporter importers["MedicalLog"] = MedicalLogImporter importers["ObservationLog"] = ObservationLogImporter - importers["SeedingLog"] = SeedingLogImporter return importers @@ -156,8 +149,6 @@ class FromFarmOS(Importer): :returns: Equivalent naive UTC ``datetime`` """ - if not dt: - return None dt = datetime.datetime.fromisoformat(dt) return self.app.make_utc(dt) @@ -339,20 +330,21 @@ class AnimalAssetImporter(AssetImporterBase): model_class = model.AnimalAsset - animal_types_by_farmos_uuid = None - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "animal_type_uuid", - "sex", - "is_sterile", - "produces_eggs", - "birthdate", - ] - ) - return fields + supported_fields = [ + "farmos_uuid", + "drupal_id", + "asset_type", + "asset_name", + "animal_type_uuid", + "sex", + "is_sterile", + "produces_eggs", + "birthdate", + "notes", + "archived", + "image_url", + "thumbnail_url", + ] def setup(self): super().setup() @@ -363,17 +355,6 @@ class AnimalAssetImporter(AssetImporterBase): if animal_type.farmos_uuid: self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type - def get_animal_type_by_farmos_uuid(self, uuid): - if self.animal_types_by_farmos_uuid is not None: - return self.animal_types_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.AnimalType) - .filter(model.AnimalType.farmos_uuid == uuid) - .first() - ) - def normalize_source_object(self, animal): """ """ animal_type_uuid = None @@ -381,7 +362,7 @@ class AnimalAssetImporter(AssetImporterBase): if animal_type := relationships.get("animal_type"): if animal_type["data"]: - if wf_animal_type := self.get_animal_type_by_farmos_uuid( + if wf_animal_type := self.animal_types_by_farmos_uuid.get( UUID(animal_type["data"]["id"]) ): animal_type_uuid = wf_animal_type.uuid @@ -418,12 +399,12 @@ class AnimalAssetImporter(AssetImporterBase): return data -class TaxonomyImporterBase(FromFarmOS, ToWutta): +class AnimalTypeImporter(FromFarmOS, ToWutta): """ - farmOS API → WuttaFarm importer for taxonomy terms + farmOS API → WuttaFarm importer for Animal Types """ - taxonomy_type = None + model_class = model.AnimalType supported_fields = [ "farmos_uuid", @@ -434,50 +415,19 @@ class TaxonomyImporterBase(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list( - self.farmos_client.resource.iterate("taxonomy_term", self.taxonomy_type) - ) + animal_types = self.farmos_client.resource.get("taxonomy_term", "animal_type") + return animal_types["data"] - def normalize_source_object(self, term): + def normalize_source_object(self, animal_type): """ """ - if description := term["attributes"]["description"]: - description = description["value"] - return { - "farmos_uuid": UUID(term["id"]), - "drupal_id": term["attributes"]["drupal_internal__tid"], - "name": term["attributes"]["name"], - "description": description, + "farmos_uuid": UUID(animal_type["id"]), + "drupal_id": animal_type["attributes"]["drupal_internal__tid"], + "name": animal_type["attributes"]["name"], + "description": animal_type["attributes"]["description"], } -class AnimalTypeImporter(TaxonomyImporterBase): - """ - farmOS API → WuttaFarm importer for Animal Types - """ - - model_class = model.AnimalType - taxonomy_type = "animal_type" - - -class MaterialTypeImporter(TaxonomyImporterBase): - """ - farmOS API → WuttaFarm importer for Material Types - """ - - model_class = model.MaterialType - taxonomy_type = "material_type" - - -class EquipmentTypeImporter(TaxonomyImporterBase): - """ - farmOS API → WuttaFarm importer for Equipment Types - """ - - model_class = model.EquipmentType - taxonomy_type = "equipment_type" - - class AssetTypeImporter(FromFarmOS, ToWutta): """ farmOS API → WuttaFarm importer for Asset Types @@ -494,7 +444,8 @@ class AssetTypeImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("asset_type")) + asset_types = self.farmos_client.resource.get("asset_type") + return asset_types["data"] def normalize_source_object(self, asset_type): """ """ @@ -506,124 +457,6 @@ class AssetTypeImporter(FromFarmOS, ToWutta): } -class EquipmentAssetImporter(AssetImporterBase): - """ - farmOS API → WuttaFarm importer for Equipment Assets - """ - - model_class = model.EquipmentAsset - - equipment_types_by_farmos_uuid = None - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "equipment_types", - ] - ) - return fields - - def setup(self): - super().setup() - model = self.app.model - - self.equipment_types_by_farmos_uuid = {} - for equipment_type in self.target_session.query(model.EquipmentType): - if equipment_type.farmos_uuid: - self.equipment_types_by_farmos_uuid[equipment_type.farmos_uuid] = ( - equipment_type - ) - - def get_equipment_type_by_farmos_uuid(self, uuid): - if self.equipment_types_by_farmos_uuid is not None: - return self.equipment_types_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.EquipmentType) - .filter_by(farmos_uuid=uuid) - .first() - ) - - def normalize_source_object(self, equipment): - """ """ - data = super().normalize_source_object(equipment) - - equipment_types = [] - if relationships := equipment.get("relationships"): - - if equipment_type := relationships.get("equipment_type"): - equipment_types = [] - for equipment_type in equipment_type["data"]: - if wf_equipment_type := self.get_equipment_type_by_farmos_uuid( - UUID(equipment_type["id"]) - ): - equipment_types.append(wf_equipment_type.uuid) - else: - log.warning( - "equipment type not found: %s", equipment_type["id"] - ) - - data.update( - { - "manufacturer": equipment["attributes"]["manufacturer"], - "model": equipment["attributes"]["model"], - "serial_number": equipment["attributes"]["serial_number"], - "equipment_types": set(equipment_types), - } - ) - return data - - def normalize_target_object(self, equipment): - data = super().normalize_target_object(equipment) - - if "equipment_types" in self.fields: - data["equipment_types"] = set( - [etype.uuid for etype in equipment.equipment_types] - ) - - return data - - def update_target_object(self, equipment, source_data, target_data=None): - model = self.app.model - equipment = super().update_target_object(equipment, source_data, target_data) - - if "equipment_types" in self.fields: - if ( - not target_data - or target_data["equipment_types"] != source_data["equipment_types"] - ): - - for uuid in source_data["equipment_types"]: - if not target_data or uuid not in target_data["equipment_types"]: - self.target_session.flush() - equipment._equipment_types.append( - model.EquipmentAssetEquipmentType(equipment_type_uuid=uuid) - ) - - if target_data: - for uuid in target_data["equipment_types"]: - if uuid not in source_data["equipment_types"]: - equipment_type = ( - self.target_session.query( - model.EquipmentAssetEquipmentType - ) - .filter( - model.EquipmentAssetEquipmentType.equipment_asset - == equipment - ) - .filter( - model.EquipmentAssetEquipmentType.equipment_type_uuid - == uuid - ) - .one() - ) - self.target_session.delete(equipment_type) - - return equipment - - class GroupAssetImporter(AssetImporterBase): """ farmOS API → WuttaFarm importer for Group Assets @@ -631,14 +464,20 @@ class GroupAssetImporter(AssetImporterBase): model_class = model.GroupAsset - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "produces_eggs", - ] - ) - return fields + supported_fields = [ + "farmos_uuid", + "drupal_id", + "asset_type", + "asset_name", + "is_location", + "is_fixed", + "produces_eggs", + "notes", + "archived", + "image_url", + "thumbnail_url", + "parents", + ] def normalize_source_object(self, group): """ """ @@ -658,16 +497,18 @@ class LandAssetImporter(AssetImporterBase): model_class = model.LandAsset - land_types_by_id = None - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "land_type_uuid", - ] - ) - return fields + supported_fields = [ + "farmos_uuid", + "drupal_id", + "asset_type", + "asset_name", + "land_type_uuid", + "is_location", + "is_fixed", + "notes", + "archived", + "parents", + ] def setup(self): """ """ @@ -678,21 +519,10 @@ class LandAssetImporter(AssetImporterBase): for land_type in self.target_session.query(model.LandType): self.land_types_by_id[land_type.drupal_id] = land_type - def get_land_type_by_id(self, drupal_id): - if self.land_types_by_id is not None: - return self.land_types_by_id.get(drupal_id) - - model = self.app.model - return ( - self.target_session.query(model.LandType) - .filter_by(drupal_id=drupal_id) - .first() - ) - def normalize_source_object(self, land): """ """ land_type_id = land["attributes"]["land_type"] - land_type = self.get_land_type_by_id(land_type_id) + land_type = self.land_types_by_id.get(land_type_id) if not land_type: log.warning( "invalid land_type '%s' for farmOS Land Asset: %s", land_type_id, land @@ -723,7 +553,8 @@ class LandTypeImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("land_type")) + land_types = self.farmos_client.resource.get("land_type") + return land_types["data"] def normalize_source_object(self, land_type): """ """ @@ -750,7 +581,8 @@ class PlantTypeImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("taxonomy_term", "plant_type")) + result = self.farmos_client.resource.get("taxonomy_term", "plant_type") + return result["data"] def normalize_source_object(self, plant_type): """ """ @@ -762,34 +594,6 @@ 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): """ farmOS API → WuttaFarm importer for Plant Assets @@ -797,18 +601,17 @@ class PlantAssetImporter(AssetImporterBase): model_class = model.PlantAsset - plant_types_by_farmos_uuid = None - seasons_by_farmos_uuid = None - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "plant_types", - "seasons", - ] - ) - return fields + supported_fields = [ + "farmos_uuid", + "drupal_id", + "asset_type", + "asset_name", + "plant_types", + "notes", + "archived", + "image_url", + "thumbnail_url", + ] def setup(self): super().setup() @@ -819,61 +622,25 @@ class PlantAssetImporter(AssetImporterBase): if plant_type.farmos_uuid: 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 get_plant_type_by_farmos_uuid(self, uuid): - if self.plant_types_by_farmos_uuid is not None: - return self.plant_types_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.PlantType) - .filter_by(farmos_uuid=uuid) - .first() - ) - - def get_season_by_farmos_uuid(self, uuid): - if self.seasons_by_farmos_uuid is not None: - return self.seasons_by_farmos_uuid.get(uuid) - - model = self.app.model - return ( - self.target_session.query(model.Season).filter_by(farmos_uuid=uuid).first() - ) - def normalize_source_object(self, plant): """ """ - data = super().normalize_source_object(plant) - plant_types = [] - seasons = [] if relationships := plant.get("relationships"): if plant_type := relationships.get("plant_type"): plant_types = [] for plant_type in plant_type["data"]: - if wf_plant_type := self.get_plant_type_by_farmos_uuid( + if wf_plant_type := self.plant_types_by_farmos_uuid.get( UUID(plant_type["id"]) ): plant_types.append(wf_plant_type.uuid) else: 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.get_season_by_farmos_uuid(UUID(season["id"])): - seasons.append(wf_season.uuid) - else: - log.warning("season not found: %s", season["id"]) - + data = super().normalize_source_object(plant) data.update( { "plant_types": set(plant_types), - "seasons": set(seasons), } ) return data @@ -884,9 +651,6 @@ class PlantAssetImporter(AssetImporterBase): if "plant_types" in self.fields: 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 def update_target_object(self, plant, source_data, target_data=None): @@ -919,25 +683,6 @@ class PlantAssetImporter(AssetImporterBase): ) 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 @@ -948,16 +693,20 @@ class StructureAssetImporter(AssetImporterBase): model_class = model.StructureAsset - structure_types_by_id = None - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "structure_type_uuid", - ] - ) - return fields + supported_fields = [ + "farmos_uuid", + "drupal_id", + "asset_type", + "asset_name", + "structure_type_uuid", + "is_location", + "is_fixed", + "notes", + "archived", + "image_url", + "thumbnail_url", + "parents", + ] def setup(self): super().setup() @@ -967,21 +716,10 @@ class StructureAssetImporter(AssetImporterBase): for structure_type in self.target_session.query(model.StructureType): self.structure_types_by_id[structure_type.drupal_id] = structure_type - def get_structure_type_by_id(self, drupal_id): - if self.structure_types_by_id is not None: - return self.structure_types_by_id.get(drupal_id) - - model = self.app.model - return ( - self.target_session.query(model.StructureType) - .filter_by(drupal_id=drupal_id) - .first() - ) - def normalize_source_object(self, structure): """ """ structure_type_id = structure["attributes"]["structure_type"] - structure_type = self.get_structure_type_by_id(structure_type_id) + structure_type = self.structure_types_by_id.get(structure_type_id) if not structure_type: log.warning( "invalid structure_type '%s' for farmOS Structure Asset: %s", @@ -1014,7 +752,8 @@ class StructureTypeImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("structure_type")) + structure_types = self.farmos_client.resource.get("structure_type") + return structure_types["data"] def normalize_source_object(self, structure_type): """ """ @@ -1025,14 +764,6 @@ class StructureTypeImporter(FromFarmOS, ToWutta): } -class WaterAssetImporter(AssetImporterBase): - """ - farmOS API → WuttaFarm importer for Water Assets - """ - - model_class = model.WaterAsset - - class UserImporter(FromFarmOS, ToWutta): """ farmOS API → WuttaFarm importer for Users @@ -1060,7 +791,8 @@ class UserImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("user")) + users = self.farmos_client.resource.get("user") + return users["data"] def normalize_source_object(self, user): """ """ @@ -1101,7 +833,6 @@ class MeasureImporter(FromFarmOS, ToWutta): supported_fields = [ "drupal_id", - "ordinal", "name", ] @@ -1112,15 +843,12 @@ class MeasureImporter(FromFarmOS, ToWutta): ) response.raise_for_status() data = response.json() - self.ordinal = 0 return data["definitions"]["attributes"]["properties"]["measure"]["oneOf"] def normalize_source_object(self, measure): """ """ - self.ordinal += 1 return { "drupal_id": measure["const"], - "ordinal": self.ordinal, "name": measure["title"], } @@ -1141,7 +869,8 @@ class UnitImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("taxonomy_term", "unit")) + result = self.farmos_client.resource.get("taxonomy_term", "unit") + return result["data"] def normalize_source_object(self, unit): """ """ @@ -1169,7 +898,8 @@ class QuantityTypeImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("quantity_type")) + result = self.farmos_client.resource.get("quantity_type") + return result["data"] def normalize_source_object(self, quantity_type): """ """ @@ -1197,7 +927,8 @@ class LogTypeImporter(FromFarmOS, ToWutta): def get_source_objects(self): """ """ - return list(self.farmos_client.resource.iterate("log_type")) + log_types = self.farmos_client.resource.get("log_type") + return log_types["data"] def normalize_source_object(self, log_type): """ """ @@ -1495,41 +1226,6 @@ class ObservationLogImporter(LogImporterBase): model_class = model.ObservationLog -class SeedingLogImporter(LogImporterBase): - """ - farmOS API → WuttaFarm importer for Seeding Logs - """ - - model_class = model.SeedingLog - - def get_simple_fields(self): - """ """ - fields = list(super().get_simple_fields()) - # nb. must explicitly declare proxy fields - fields.extend( - [ - "source", - "purchase_date", - "lot_number", - ] - ) - return fields - - def normalize_source_object(self, log): - """ """ - data = super().normalize_source_object(log) - data.update( - { - "source": log["attributes"]["source"], - "purchase_date": self.normalize_datetime( - log["attributes"]["purchase_date"] - ), - "lot_number": log["attributes"]["lot_number"], - } - ) - return data - - class QuantityImporterBase(FromFarmOS, ToWutta): """ Base class for farmOS API → WuttaFarm quantity importers @@ -1575,7 +1271,8 @@ class QuantityImporterBase(FromFarmOS, ToWutta): def get_source_objects(self): """ """ quantity_type = self.get_farmos_quantity_type() - return list(self.farmos_client.resource.iterate("quantity", quantity_type)) + result = self.farmos_client.resource.get("quantity", quantity_type) + return result["data"] def get_quantity_type_by_farmos_uuid(self, uuid): if hasattr(self, "quantity_types_by_farmos_uuid"): @@ -1658,76 +1355,3 @@ class StandardQuantityImporter(QuantityImporterBase): "units_uuid", "label", ] - - -class MaterialQuantityImporter(QuantityImporterBase): - """ - farmOS API → WuttaFarm importer for Material Quantities - """ - - model_class = model.MaterialQuantity - - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "material_types", - ] - ) - return fields - - def normalize_source_object(self, quantity): - """ """ - data = super().normalize_source_object(quantity) - - if "material_types" in self.fields: - data["material_types"] = [ - UUID(mtype["id"]) - for mtype in quantity["relationships"]["material_type"]["data"] - ] - - return data - - def normalize_target_object(self, quantity): - data = super().normalize_target_object(quantity) - - if "material_types" in self.fields: - data["material_types"] = [ - mtype.farmos_uuid for mtype in quantity.material_types - ] - - return data - - def update_target_object(self, quantity, source_data, target_data=None): - model = self.app.model - quantity = super().update_target_object(quantity, source_data, target_data) - - if "material_types" in self.fields: - if ( - not target_data - or target_data["material_types"] != source_data["material_types"] - ): - - for farmos_uuid in source_data["material_types"]: - if ( - not target_data - or farmos_uuid not in target_data["material_types"] - ): - mtype = ( - self.target_session.query(model.MaterialType) - .filter(model.MaterialType.farmos_uuid == farmos_uuid) - .one() - ) - quantity.material_types.append(mtype) - - if target_data: - for farmos_uuid in target_data["material_types"]: - if farmos_uuid not in source_data["material_types"]: - mtype = ( - self.target_session.query(model.MaterialType) - .filter(model.MaterialType.farmos_uuid == farmos_uuid) - .one() - ) - quantity.material_types.remove(mtype) - - return quantity diff --git a/src/wuttafarm/normal.py b/src/wuttafarm/normal.py index c47fcc3..4fc8796 100644 --- a/src/wuttafarm/normal.py +++ b/src/wuttafarm/normal.py @@ -84,41 +84,16 @@ class Normalizer(GenericHandler): self._farmos_units = units return self._farmos_units - def normalize_datetime(self, value): - if not value: - return None - value = datetime.datetime.fromisoformat(value) - return self.app.localtime(value) - def normalize_farmos_asset(self, asset, included={}): """ """ if notes := asset["attributes"]["notes"]: notes = notes["value"] - parent_objects = [] - parent_uuids = [] owner_objects = [] owner_uuids = [] if relationships := asset.get("relationships"): - if parents := relationships.get("parent"): - for parent in parents["data"]: - parent_uuid = parent["id"] - parent_uuids.append(parent_uuid) - parent_object = { - "uuid": parent_uuid, - "type": parent["type"], - "asset_type": parent["type"].split("--")[1], - } - if parent := included.get(parent_uuid): - parent_object.update( - { - "name": parent["attributes"]["name"], - } - ) - parent_objects.append(parent_object) - if owners := relationships.get("owner"): for user in owners["data"]: user_uuid = user["id"] @@ -131,11 +106,6 @@ class Normalizer(GenericHandler): } ) - # if self.farmos_4x: - # archived = asset["attributes"]["archived"] - # else: - # archived = asset["attributes"]["status"] == "archived" - return { "uuid": asset["id"], "drupal_id": asset["attributes"]["drupal_internal__id"], @@ -144,10 +114,6 @@ class Normalizer(GenericHandler): "is_fixed": asset["attributes"]["is_fixed"], "archived": asset["attributes"]["archived"], "notes": notes, - # nb. this is only used for certain asset types - "produces_eggs": asset["attributes"].get("produces_eggs"), - "parents": parent_objects, - "parent_uuids": parent_uuids, "owners": owner_objects, "owner_uuids": owner_uuids, } @@ -155,7 +121,8 @@ class Normalizer(GenericHandler): def normalize_farmos_log(self, log, included={}): if timestamp := log["attributes"]["timestamp"]: - timestamp = self.normalize_datetime(timestamp) + timestamp = datetime.datetime.fromisoformat(timestamp) + timestamp = self.app.localtime(timestamp) if notes := log["attributes"]["notes"]: notes = notes["value"] @@ -265,29 +232,27 @@ class Normalizer(GenericHandler): measure_id = attrs["measure"] - quantity_object = { - "uuid": quantity["id"], - "drupal_id": attrs["drupal_internal__id"], - "quantity_type_uuid": rels["quantity_type"]["data"]["id"], - "quantity_type_id": rels["quantity_type"]["data"]["meta"][ - "drupal_internal__target_id" - ], - "measure_id": measure_id, - "measure_name": self.get_farmos_measure_name(measure_id), - "value_numerator": value["numerator"], - "value_decimal": value["decimal"], - "value_denominator": value["denominator"], - "unit_uuid": unit_uuid, - "unit_name": unit["attributes"]["name"], - } - if quantity_object["quantity_type_id"] == "material": - quantity_object["material_types"] = [ - {"uuid": mtype["id"]} - for mtype in quantity["relationships"]["material_type"][ - "data" - ] - ] - quantity_objects.append(quantity_object) + quantity_objects.append( + { + "uuid": quantity["id"], + "drupal_id": attrs["drupal_internal__id"], + "quantity_type_uuid": rels["quantity_type"]["data"][ + "id" + ], + "quantity_type_id": rels["quantity_type"]["data"][ + "meta" + ]["drupal_internal__target_id"], + "measure_id": measure_id, + "measure_name": self.get_farmos_measure_name( + measure_id + ), + "value_numerator": value["numerator"], + "value_decimal": value["decimal"], + "value_denominator": value["denominator"], + "unit_uuid": unit_uuid, + "unit_name": unit["attributes"]["name"], + } + ) if owners := relationships.get("owner"): for user in owners["data"]: diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py index e24f0cf..6bf434e 100644 --- a/src/wuttafarm/web/forms/schema.py +++ b/src/wuttafarm/web/forms/schema.py @@ -28,7 +28,7 @@ import json import colander from wuttaweb.db import Session -from wuttaweb.forms.schema import ObjectRef, WuttaSet, WuttaList +from wuttaweb.forms.schema import ObjectRef, WuttaSet from wuttaweb.forms.widgets import NotesWidget @@ -164,9 +164,10 @@ class FarmOSRefs(WuttaSet): self.route_prefix = route_prefix def serialize(self, node, appstruct): - if not appstruct: + if appstruct is colander.null: return colander.null - return appstruct + + return json.dumps(appstruct) def widget_maker(self, **kwargs): from wuttafarm.web.forms.widgets import FarmOSRefsWidget @@ -216,35 +217,6 @@ class FarmOSQuantityRefs(WuttaSet): return FarmOSQuantityRefsWidget(**kwargs) -class FarmOSTaxonomyTerms(colander.SchemaType): - """ - Schema type which can represent multiple taxonomy terms. - """ - - route_prefix = None - - def __init__(self, request, route_prefix=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - if route_prefix: - self.route_prefix = route_prefix - - def serialize(self, node, appstruct): - if not appstruct: - return colander.null - return appstruct - - def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import FarmOSTaxonomyTermsWidget - - return FarmOSTaxonomyTermsWidget(self.request, self.route_prefix, **kwargs) - - -class FarmOSEquipmentTypeRefs(FarmOSTaxonomyTerms): - - route_prefix = "farmos_equipment_types" - - class FarmOSPlantTypes(colander.SchemaType): def __init__(self, request, *args, **kwargs): @@ -289,35 +261,6 @@ class LandTypeRef(ObjectRef): return self.request.route_url("land_types.view", uuid=land_type.uuid) -class TaxonomyTermRefs(WuttaList): - """ - Generic schema type for a field which can reference multiple - taxonomy terms. - """ - - def serialize(self, node, appstruct): - if not appstruct: - return colander.null - - terms = [] - for term in appstruct: - terms.append( - { - "uuid": str(term.uuid), - "name": term.name, - } - ) - return terms - - -class EquipmentTypeRefs(TaxonomyTermRefs): - - def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import EquipmentTypeRefsWidget - - return EquipmentTypeRefsWidget(self.request, **kwargs) - - class PlantTypeRefs(WuttaSet): """ Schema type for Plant Types field (on a Plant Asset). @@ -345,62 +288,6 @@ class PlantTypeRefs(WuttaSet): return PlantTypeRefsWidget(self.request, **kwargs) -class MaterialTypeRefs(colander.List): - """ - Schema type for Material Types field (on a Material Asset). - """ - - def __init__(self, request): - super().__init__() - self.request = request - self.config = self.request.wutta_config - self.app = self.config.get_app() - - def serialize(self, node, appstruct): - if not appstruct: - return colander.null - - mtypes = [] - for mtype in appstruct: - mtypes.append( - { - "uuid": mtype.uuid.hex, - "name": mtype.name, - } - ) - return mtypes - - def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import MaterialTypeRefsWidget - - return MaterialTypeRefsWidget(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): def __init__(self, request, *args, **kwargs): @@ -485,100 +372,55 @@ class UsersType(colander.SchemaType): return UsersWidget(self.request, **kwargs) +class AssetParentRefs(WuttaSet): + """ + Schema type for Parents field which references assets. + """ + + def serialize(self, node, appstruct): + if not appstruct: + appstruct = [] + uuids = [u.hex for u in appstruct] + return json.dumps(uuids) + + def widget_maker(self, **kwargs): + from wuttafarm.web.forms.widgets import AssetParentRefsWidget + + return AssetParentRefsWidget(self.request, **kwargs) + + class AssetRefs(WuttaSet): """ Schema type for Assets field (on a Log record) """ - def __init__( - self, request, for_asset=None, is_group=None, is_location=None, **kwargs - ): - super().__init__(request, **kwargs) - self.is_group = is_group - self.is_location = is_location - self.for_asset = for_asset - def serialize(self, node, appstruct): if not appstruct: return colander.null - return {asset.uuid.hex for asset in appstruct} + return {asset.uuid for asset in appstruct} def widget_maker(self, **kwargs): from wuttafarm.web.forms.widgets import AssetRefsWidget - model = self.app.model - session = Session() - - if "values" not in kwargs: - query = session.query(model.Asset) - if self.is_group is not None: - query = query.join(model.GroupAsset) - if self.is_location is not None: - query = query.filter(model.Asset.is_location == self.is_location) - if self.for_asset: - query = query.filter(model.Asset.uuid != self.for_asset.uuid) - query = query.order_by(model.Asset.asset_name) - values = [(asset.uuid.hex, str(asset)) for asset in query] - kwargs["values"] = values - return AssetRefsWidget(self.request, **kwargs) -class QuantityRefs(colander.List): +class LogQuantityRefs(WuttaSet): """ Schema type for Quantities field (on a Log record) """ - def __init__(self, request): - super().__init__() - self.request = request - self.config = self.request.wutta_config - self.app = self.config.get_app() - def serialize(self, node, appstruct): if not appstruct: return colander.null - quantities = [] - for qty in appstruct: - - quantity = { - "uuid": qty.uuid.hex, - "quantity_type": { - "drupal_id": qty.quantity_type_id, - "name": qty.quantity_type.name, - }, - "measure": qty.measure_id, - "value": qty.get_value_decimal(), - "units": { - "uuid": qty.units.uuid.hex, - "name": qty.units.name, - }, - "as_text": qty.render_as_text(self.config), - # nb. always include this regardless of quantity type, - # for sake of easier frontend logic - "material_types": [], - } - - if qty.quantity_type_id == "material": - quantity["material_types"] = [] - for mtype in qty.material_types: - quantity["material_types"].append( - { - "uuid": mtype.uuid.hex, - "name": mtype.name, - } - ) - - quantities.append(quantity) - - return quantities + return {qty.uuid for qty in appstruct} def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import QuantityRefsWidget + from wuttafarm.web.forms.widgets import LogQuantityRefsWidget - return QuantityRefsWidget(self.request, **kwargs) + return LogQuantityRefsWidget(self.request, **kwargs) class OwnerRefs(WuttaSet): diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py index db79eae..0a14638 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -33,7 +33,6 @@ from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget from wuttaweb.db import Session from wuttafarm.web.util import render_quantity_objects -from wuttafarm.db.model import EquipmentType class ImageWidget(Widget): @@ -125,7 +124,7 @@ class FarmOSRefsWidget(Widget): return HTML.tag("span") links = [] - for obj in cstruct: + for obj in json.loads(cstruct): url = self.request.route_url( f"{self.route_prefix}.view", uuid=obj["uuid"] ) @@ -229,38 +228,6 @@ class FarmOSUnitRefWidget(Widget): return super().serialize(field, cstruct, **kw) -class FarmOSTaxonomyTermsWidget(Widget): - """ - Widget to display a field which can reference multiple taxonomy - terms. - """ - - def __init__(self, request, route_prefix, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.route_prefix = route_prefix - - def serialize(self, field, cstruct, **kw): - """ """ - readonly = kw.get("readonly", self.readonly) - if readonly: - if cstruct in (colander.null, None): - return HTML.tag("span") - - links = [] - for term in cstruct: - link = tags.link_to( - term["name"], - self.request.route_url( - f"{self.route_prefix}.view", uuid=term["uuid"] - ), - ) - links.append(HTML.tag("li", c=link)) - return HTML.tag("ul", c=links) - - return super().serialize(field, cstruct, **kw) - - class FarmOSPlantTypesWidget(Widget): """ Widget to display a farmOS "plant types" field. @@ -291,88 +258,6 @@ class FarmOSPlantTypesWidget(Widget): return super().serialize(field, cstruct, **kw) -class TaxonomyTermRefsWidget(Widget): - """ - Generic (incomplete) widget for fields which can reference - multiple taxonomy terms. - - This widget can handle typical read-only scenarios but the - editable mode is not implemented. - """ - - route_prefix = None - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.config = self.request.wutta_config - self.app = self.config.get_app() - - @classmethod - def get_route_prefix(cls): - return cls.route_prefix - - @classmethod - def get_permission_prefix(cls): - return cls.route_prefix - - def serialize(self, field, cstruct, **kw): - """ """ - if not cstruct: - cstruct = [] - - if readonly := kw.get("readonly", self.readonly): - items = [] - route_prefix = self.get_route_prefix() - for term in cstruct: - url = self.request.route_url(f"{route_prefix}.view", uuid=term["uuid"]) - link = tags.link_to(term["name"], url) - items.append(HTML.tag("li", c=link)) - return HTML.tag("ul", c=items) - - tmpl_values = self.get_template_values(field, cstruct, kw) - return field.renderer(self.template, **tmpl_values) - - def get_template_values(self, field, cstruct, kw): - values = super().get_template_values(field, cstruct, kw) - model = self.app.model - session = Session() - - terms = [] - query = session.query(self.model_class).order_by(self.model_class.name) - for term in query: - terms.append( - { - "uuid": str(term.uuid), - "name": term.name, - } - ) - values["terms"] = terms - - permission_prefix = self.get_permission_prefix() - if self.request.has_perm(f"{permission_prefix}.create"): - values["can_create"] = True - - return values - - def deserialize(self, field, pstruct): - """ """ - if not pstruct: - return colander.null - - return json.loads(pstruct) - - -class EquipmentTypeRefsWidget(TaxonomyTermRefsWidget): - """ - Widget for Equipment Types field. - """ - - model_class = EquipmentType - route_prefix = "equipment_types" - template = "equipmenttyperefs" - - class PlantTypeRefsWidget(Widget): """ Widget for Plant Types field (on a Plant Asset). @@ -447,144 +332,6 @@ class PlantTypeRefsWidget(Widget): return set(pstruct.split(",")) -class MaterialTypeRefsWidget(Widget): - """ - Widget for Material Types field (on a Material Asset). - """ - - template = "materialtyperefs" - 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 not cstruct: - cstruct = [] - - if readonly := kw.get("readonly", self.readonly): - items = [] - for mtype in cstruct: - items.append( - HTML.tag( - "li", - c=tags.link_to( - mtype["name"], - self.request.route_url( - "material_types.view", uuid=mtype["uuid"] - ), - ), - ) - ) - return HTML.tag("ul", c=items) - - tmpl_values = self.get_template_values(field, cstruct, kw) - return field.renderer(self.template, **tmpl_values) - - def get_template_values(self, field, cstruct, kw): - """ """ - values = super().get_template_values(field, cstruct, kw) - session = Session() - - material_types = [] - for mtype in self.app.get_material_types(session): - material_types.append( - { - "uuid": mtype.uuid.hex, - "name": mtype.name, - } - ) - values["material_types"] = json.dumps(material_types) - - return values - - def deserialize(self, field, pstruct): - """ """ - if not pstruct: - return [] - - return json.loads(pstruct) - - -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): """ Widget to display a "structure" field. @@ -646,20 +393,42 @@ class UsersWidget(Widget): ############################## -class AssetRefsWidget(Widget): +class AssetParentRefsWidget(WuttaCheckboxChoiceWidget): + """ + Widget for Parents field which references assets. + """ + + def serialize(self, field, cstruct, **kw): + """ """ + model = self.app.model + session = Session() + + readonly = kw.get("readonly", self.readonly) + if readonly: + parents = [] + for uuid in json.loads(cstruct): + parent = session.get(model.Asset, uuid) + parents.append( + HTML.tag( + "li", + c=tags.link_to( + str(parent), + self.request.route_url( + f"{parent.asset_type}_assets.view", uuid=parent.uuid + ), + ), + ) + ) + return HTML.tag("ul", c=parents) + + return super().serialize(field, cstruct, **kw) + + +class AssetRefsWidget(WuttaCheckboxChoiceWidget): """ Widget for Assets field (of various kinds). """ - template = "assetrefs" - values = () - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.config = self.request.wutta_config - self.app = self.config.get_app() - def serialize(self, field, cstruct, **kw): """ """ model = self.app.model @@ -683,43 +452,14 @@ class AssetRefsWidget(Widget): ) return HTML.tag("ul", c=assets) - values = kw.get("values", self.values) - if not isinstance(values, sequence_types): - raise TypeError("Values must be a sequence type (list, tuple, or range).") - - kw["values"] = _normalize_choices(values) - tmpl_values = self.get_template_values(field, cstruct, kw) - return field.renderer(self.template, **tmpl_values) - - def get_template_values(self, field, cstruct, kw): - """ """ - values = super().get_template_values(field, cstruct, kw) - - values["js_values"] = json.dumps(values["values"]) - - return values - - def deserialize(self, field, pstruct): - """ """ - if not pstruct: - return set() - - return set(pstruct.split(",")) + return super().serialize(field, cstruct, **kw) -class QuantityRefsWidget(Widget): +class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget): """ Widget for Quantities field (on a Log record) """ - template = "quantityrefs" - - 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 @@ -727,78 +467,24 @@ class QuantityRefsWidget(Widget): readonly = kw.get("readonly", self.readonly) if readonly: - if not cstruct: - return "" - quantities = [] - for qty in cstruct: - url = self.request.route_url( - f"quantities_{qty['quantity_type']['drupal_id']}.view", - uuid=qty["uuid"], + for uuid in cstruct or []: + qty = session.get(model.Quantity, uuid) + quantities.append( + HTML.tag( + "li", + c=tags.link_to( + qty.render_as_text(self.config), + # TODO + self.request.route_url( + "quantities_standard.view", uuid=qty.uuid + ), + ), + ) ) - quantities.append(HTML.tag("li", c=tags.link_to(qty["as_text"], url))) - return HTML.tag("ul", c=quantities) - tmpl_values = self.get_template_values(field, cstruct, kw) - return field.renderer(self.template, **tmpl_values) - - def get_template_values(self, field, cstruct, kw): - model = self.app.model - session = Session() - values = super().get_template_values(field, cstruct, kw) - - qtypes = [] - for qtype in self.app.get_quantity_types(session): - qtypes.append( - { - "uuid": qtype.uuid.hex, - "drupal_id": qtype.drupal_id, - "name": qtype.name, - } - ) - values["quantity_types"] = qtypes - - material_types = [] - for mtype in self.app.get_material_types(session): - material_types.append( - { - "uuid": mtype.uuid.hex, - "name": mtype.name, - } - ) - values["material_types"] = material_types - - measures = [] - for measure in self.app.get_measures(session): - measures.append( - { - "uuid": measure.uuid.hex, - "drupal_id": measure.drupal_id, - "name": measure.name, - } - ) - values["measures"] = measures - - units = [] - for unit in self.app.get_units(session): - units.append( - { - "uuid": unit.uuid.hex, - "drupal_id": unit.drupal_id, - "name": unit.name, - } - ) - values["units"] = units - - return values - - def deserialize(self, field, pstruct): - """ """ - if not pstruct: - return set() - - return json.loads(pstruct) + return super().serialize(field, cstruct, **kw) class OwnerRefsWidget(WuttaCheckboxChoiceWidget): diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index 2756738..fe7719e 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -92,11 +92,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "animal_assets", "perm": "animal_assets.list", }, - { - "title": "Equipment", - "route": "equipment_assets", - "perm": "equipment_assets.list", - }, { "title": "Group", "route": "group_assets", @@ -117,22 +112,12 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "structure_assets", "perm": "structure_assets.list", }, - { - "title": "Water", - "route": "water_assets", - "perm": "water_assets.list", - }, {"type": "sep"}, { "title": "Animal Types", "route": "animal_types", "perm": "animal_types.list", }, - { - "title": "Equipment Types", - "route": "equipment_types", - "perm": "equipment_types.list", - }, { "title": "Land Types", "route": "land_types", @@ -143,11 +128,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "plant_types", "perm": "plant_types.list", }, - { - "title": "Seasons", - "route": "seasons", - "perm": "seasons.list", - }, { "title": "Structure Types", "route": "structure_types", @@ -191,22 +171,12 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "logs_observation", "perm": "logs_observation.list", }, - { - "title": "Seeding", - "route": "logs_seeding", - "perm": "logs_seeding.list", - }, {"type": "sep"}, { "title": "All Quantities", "route": "quantities", "perm": "quantities.list", }, - { - "title": "Material Quantities", - "route": "quantities_material", - "perm": "quantities_material.list", - }, { "title": "Standard Quantities", "route": "quantities_standard", @@ -218,11 +188,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "log_types", "perm": "log_types.list", }, - { - "title": "Material Types", - "route": "material_types", - "perm": "material_types.list", - }, { "title": "Measures", "route": "measures", @@ -259,11 +224,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_animal_assets", "perm": "farmos_animal_assets.list", }, - { - "title": "Equipment Assets", - "route": "farmos_equipment_assets", - "perm": "farmos_equipment_assets.list", - }, { "title": "Group Assets", "route": "farmos_group_assets", @@ -284,11 +244,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_structure_assets", "perm": "farmos_structure_assets.list", }, - { - "title": "Water Assets", - "route": "farmos_water_assets", - "perm": "farmos_water_assets.list", - }, {"type": "sep"}, { "title": "Activity Logs", @@ -310,22 +265,12 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_logs_observation", "perm": "farmos_logs_observation.list", }, - { - "title": "Seeding Logs", - "route": "farmos_logs_seeding", - "perm": "farmos_logs_seeding.list", - }, {"type": "sep"}, { "title": "Animal Types", "route": "farmos_animal_types", "perm": "farmos_animal_types.list", }, - { - "title": "Equipment Types", - "route": "farmos_equipment_types", - "perm": "farmos_equipment_types.list", - }, { "title": "Land Types", "route": "farmos_land_types", @@ -336,11 +281,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_plant_types", "perm": "farmos_plant_types.list", }, - { - "title": "Seasons", - "route": "farmos_seasons", - "perm": "farmos_seasons.list", - }, { "title": "Structure Types", "route": "farmos_structure_types", @@ -357,21 +297,11 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_log_types", "perm": "farmos_log_types.list", }, - { - "title": "Material Types", - "route": "farmos_material_types", - "perm": "farmos_material_types.list", - }, { "title": "Quantity Types", "route": "farmos_quantity_types", "perm": "farmos_quantity_types.list", }, - { - "title": "Material Quantities", - "route": "farmos_quantities_material", - "perm": "farmos_quantities_material.list", - }, { "title": "Standard Quantities", "route": "farmos_quantities_standard", @@ -403,11 +333,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_animal_assets", "perm": "farmos_animal_assets.list", }, - { - "title": "Equipment", - "route": "farmos_equipment_assets", - "perm": "farmos_equipment_assets.list", - }, { "title": "Group", "route": "farmos_group_assets", @@ -428,22 +353,12 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_structure_assets", "perm": "farmos_structure_assets.list", }, - { - "title": "Water", - "route": "farmos_water_assets", - "perm": "farmos_water_assets.list", - }, {"type": "sep"}, { "title": "Animal Types", "route": "farmos_animal_types", "perm": "farmos_animal_types.list", }, - { - "title": "Equipment Types", - "route": "farmos_equipment_types", - "perm": "farmos_equipment_types.list", - }, { "title": "Land Types", "route": "farmos_land_types", @@ -454,11 +369,6 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_plant_types", "perm": "farmos_plant_types.list", }, - { - "title": "Seasons", - "route": "farmos_seasons", - "perm": "farmos_seasons.list", - }, { "title": "Structure Types", "route": "farmos_structure_types", @@ -500,32 +410,17 @@ class WuttaFarmMenuHandler(base.MenuHandler): "route": "farmos_logs_observation", "perm": "farmos_logs_observation.list", }, - { - "title": "Seeding", - "route": "farmos_logs_seeding", - "perm": "farmos_logs_seeding.list", - }, {"type": "sep"}, { "title": "Log Types", "route": "farmos_log_types", "perm": "farmos_log_types.list", }, - { - "title": "Material Types", - "route": "farmos_material_types", - "perm": "farmos_material_types.list", - }, { "title": "Quantity Types", "route": "farmos_quantity_types", "perm": "farmos_quantity_types.list", }, - { - "title": "Material Quantities", - "route": "farmos_quantities_material", - "perm": "farmos_quantities_material.list", - }, { "title": "Standard Quantities", "route": "farmos_quantities_standard", diff --git a/src/wuttafarm/web/templates/appinfo/configure.mako b/src/wuttafarm/web/templates/appinfo/configure.mako index 26d6a54..912eef0 100644 --- a/src/wuttafarm/web/templates/appinfo/configure.mako +++ b/src/wuttafarm/web/templates/appinfo/configure.mako @@ -57,11 +57,6 @@ - - - - % endif -
- - ## main form -
- ${parent.page_content()} -
- - ## location map - % if map_polygon: -
- % endif - -
- - - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - % if map_polygon: - - % endif + ${parent.page_content()} diff --git a/src/wuttafarm/web/templates/base.mako b/src/wuttafarm/web/templates/base.mako index caa5c67..b28b52f 100644 --- a/src/wuttafarm/web/templates/base.mako +++ b/src/wuttafarm/web/templates/base.mako @@ -1,16 +1,6 @@ <%inherit file="wuttaweb:templates/base.mako" /> <%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" /> -<%def name="head_tags()"> - ${parent.head_tags()} - - ## TODO: this likely does not belong in the base template, and should be - ## included per template where actually needed. but this is easier for now. - - - - - <%def name="index_title_controls()"> ${parent.index_title_controls()} diff --git a/src/wuttafarm/web/templates/deform/assetrefs.pt b/src/wuttafarm/web/templates/deform/assetrefs.pt deleted file mode 100644 index b2e9660..0000000 --- a/src/wuttafarm/web/templates/deform/assetrefs.pt +++ /dev/null @@ -1,11 +0,0 @@ -
- - - -
diff --git a/src/wuttafarm/web/templates/deform/equipmenttyperefs.pt b/src/wuttafarm/web/templates/deform/equipmenttyperefs.pt deleted file mode 100644 index 4d48fd7..0000000 --- a/src/wuttafarm/web/templates/deform/equipmenttyperefs.pt +++ /dev/null @@ -1,13 +0,0 @@ -
- - - -
diff --git a/src/wuttafarm/web/templates/deform/materialtyperefs.pt b/src/wuttafarm/web/templates/deform/materialtyperefs.pt deleted file mode 100644 index 44ac6e8..0000000 --- a/src/wuttafarm/web/templates/deform/materialtyperefs.pt +++ /dev/null @@ -1,13 +0,0 @@ -
- - - -
diff --git a/src/wuttafarm/web/templates/deform/quantityrefs.pt b/src/wuttafarm/web/templates/deform/quantityrefs.pt deleted file mode 100644 index cc65f77..0000000 --- a/src/wuttafarm/web/templates/deform/quantityrefs.pt +++ /dev/null @@ -1,14 +0,0 @@ -
- - - -
diff --git a/src/wuttafarm/web/templates/deform/seasonrefs.pt b/src/wuttafarm/web/templates/deform/seasonrefs.pt deleted file mode 100644 index 955241a..0000000 --- a/src/wuttafarm/web/templates/deform/seasonrefs.pt +++ /dev/null @@ -1,13 +0,0 @@ -
- - - -
diff --git a/src/wuttafarm/web/templates/wuttafarm-components.mako b/src/wuttafarm/web/templates/wuttafarm-components.mako index 890568f..37b176e 100644 --- a/src/wuttafarm/web/templates/wuttafarm-components.mako +++ b/src/wuttafarm/web/templates/wuttafarm-components.mako @@ -1,330 +1,7 @@ <%def name="make_wuttafarm_components()"> - ${self.make_taxonomy_terms_picker_component()} - ${self.make_equipment_types_picker_component()} - ${self.make_assets_picker_component()} ${self.make_animal_type_picker_component()} - ${self.make_material_types_picker_component()} - ${self.make_quantity_editor_component()} - ${self.make_quantities_editor_component()} ${self.make_plant_types_picker_component()} - ${self.make_seasons_picker_component()} - - -<%def name="make_taxonomy_terms_picker_component()"> - - - - -<%def name="make_equipment_types_picker_component()"> - - - - -<%def name="make_assets_picker_component()"> - - <%def name="make_animal_type_picker_component()"> @@ -431,10 +108,7 @@ createSave() { this.createSaving = true - ## TODO - % if not app.is_farmos_wrapper(): const url = "${url('animal_types.ajax_create')}" - % endif const params = {name: this.createName} this.wuttaPOST(url, params, response => { this.internalAnimalTypes.push([response.data.uuid, response.data.name]) @@ -454,503 +128,6 @@ -<%def name="make_material_types_picker_component()"> - - - - -<%def name="make_quantity_editor_component()"> - - - - -<%def name="make_quantities_editor_component()"> - - - - <%def name="make_plant_types_picker_component()"> - -<%def name="make_seasons_picker_component()"> - - - diff --git a/src/wuttafarm/web/util.py b/src/wuttafarm/web/util.py index ec88525..977550a 100644 --- a/src/wuttafarm/web/util.py +++ b/src/wuttafarm/web/util.py @@ -78,12 +78,4 @@ def render_quantity_object(quantity): measure = quantity["measure_name"] value = quantity["value_decimal"] unit = quantity["unit_name"] - text = f"( {measure} ) {value} {unit}" - - if quantity["quantity_type_id"] == "material": - materials = ", ".join( - [mtype.get("name", "???") for mtype in quantity["material_types"]] - ) - return f"{materials} {text}" - - return text + return f"( {measure} ) {value} {unit}" diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index b663cf5..0d58a72 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -25,7 +25,7 @@ WuttaFarm Views from wuttaweb.views import essential -from .master import WuttaFarmMasterView, TaxonomyMasterView +from .master import WuttaFarmMasterView def includeme(config): @@ -48,23 +48,19 @@ def includeme(config): # native table views if mode != enum.FARMOS_INTEGRATION_MODE_WRAPPER: config.include("wuttafarm.web.views.units") - config.include("wuttafarm.web.views.material_types") config.include("wuttafarm.web.views.quantities") config.include("wuttafarm.web.views.asset_types") config.include("wuttafarm.web.views.assets") config.include("wuttafarm.web.views.land") config.include("wuttafarm.web.views.structures") - config.include("wuttafarm.web.views.equipment") config.include("wuttafarm.web.views.animals") config.include("wuttafarm.web.views.groups") config.include("wuttafarm.web.views.plants") - config.include("wuttafarm.web.views.water") config.include("wuttafarm.web.views.logs") config.include("wuttafarm.web.views.logs_activity") config.include("wuttafarm.web.views.logs_harvest") config.include("wuttafarm.web.views.logs_medical") config.include("wuttafarm.web.views.logs_observation") - config.include("wuttafarm.web.views.logs_seeding") # quick form views # (nb. these work with all integration modes) @@ -73,7 +69,3 @@ def includeme(config): # views for farmOS if mode != enum.FARMOS_INTEGRATION_MODE_NONE: config.include("wuttafarm.web.views.farmos") - - # webhook views (only for mirror mode) - if mode == enum.FARMOS_INTEGRATION_MODE_MIRROR: - config.include("wuttafarm.web.views.webhooks") diff --git a/src/wuttafarm/web/views/animals.py b/src/wuttafarm/web/views/animals.py index ad9f060..f4c97e2 100644 --- a/src/wuttafarm/web/views/animals.py +++ b/src/wuttafarm/web/views/animals.py @@ -23,12 +23,9 @@ Master view for Animals """ -from collections import OrderedDict - from webhelpers2.html import tags from wuttaweb.forms.schema import WuttaDictEnum -from wuttaweb.forms.widgets import WuttaDateTimeWidget from wuttaweb.util import get_form_data from wuttafarm.db.model import AnimalType, AnimalAsset @@ -237,6 +234,27 @@ class AnimalAssetView(AssetMasterView): "archived", ] + form_fields = [ + "asset_name", + "animal_type", + "birthdate", + "produces_eggs", + "sex", + "is_sterile", + "notes", + "asset_type", + "owners", + "locations", + "groups", + "archived", + "drupal_id", + "farmos_uuid", + "thumbnail_url", + "image_url", + "thumbnail", + "image", + ] + def configure_grid(self, grid): g = grid super().configure_grid(g) @@ -270,32 +288,15 @@ class AnimalAssetView(AssetMasterView): animal = f.model_instance # animal_type - f.fields.insert_after("asset_name", "animal_type") f.set_node("animal_type", AnimalTypeRef(self.request)) - # birthdate - f.fields.insert_after("animal_type", "birthdate") - # TODO: why must we assign the widget here? pretty sure that - # was not needed when we declared form_fields directly, i.e. - # instead of adding birthdate field in this method - f.set_widget("birthdate", WuttaDateTimeWidget(self.request)) - - # produces_eggs - f.fields.insert_after("birthdate", "produces_eggs") - # sex - f.fields.insert_after("produces_eggs", "sex") if not (self.creating or self.editing) and animal.sex is None: pass # TODO: dict enum widget does not handle null values well else: - # nb. ensure empty option appears like we want - sex_enum = OrderedDict([("", "N/A")] + list(enum.ANIMAL_SEX.items())) - f.set_node("sex", WuttaDictEnum(self.request, sex_enum)) + f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX)) f.set_required("sex", False) - # is_sterile - f.fields.insert_after("sex", "is_sterile") - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py index 1ada778..b4e4d31 100644 --- a/src/wuttafarm/web/views/assets.py +++ b/src/wuttafarm/web/views/assets.py @@ -23,7 +23,6 @@ Master view for Assets """ -import re from collections import OrderedDict from webhelpers2.html import tags @@ -33,7 +32,7 @@ from wuttaweb.db import Session from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.db.model import Asset, Log -from wuttafarm.web.forms.schema import OwnerRefs, AssetRefs +from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs from wuttafarm.web.forms.widgets import ImageWidget from wuttafarm.util import get_log_type_enum from wuttafarm.web.util import get_farmos_client_for_user @@ -78,25 +77,6 @@ class AssetMasterView(WuttaFarmMasterView): "archived": {"active": True, "verb": "is_false"}, } - form_fields = [ - "asset_name", - "parents", - "notes", - "asset_type", - "owners", - "is_location", - "is_fixed", - "locations", - "groups", - "archived", - "drupal_id", - "farmos_uuid", - "thumbnail_url", - "image_url", - "thumbnail", - "image", - ] - has_rows = True row_model_class = Log rows_viewable = True @@ -281,11 +261,11 @@ class AssetMasterView(WuttaFarmMasterView): f.set_default("groups", asset_handler.get_groups(asset)) # parents - f.set_node("parents", AssetRefs(self.request, for_asset=asset)) - f.set_required("parents", False) - if not self.creating: - # nb. must explicity declare value for non-standard field - f.set_default("parents", asset.parents) + if self.creating or self.editing: + f.remove("parents") # TODO: add support for this + else: + f.set_node("parents", AssetParentRefs(self.request)) + f.set_default("parents", [p.uuid for p in asset.parents]) # notes f.set_widget("notes", "notes") @@ -313,29 +293,11 @@ class AssetMasterView(WuttaFarmMasterView): f.set_default("image", asset.image_url) def objectify(self, form): - model = self.app.model - session = self.Session() asset = super().objectify(form) - data = form.validated if self.creating: asset.asset_type = self.get_asset_type() - current = [p.uuid for p in asset.parents] - desired = data["parents"] or [] - - for uuid in desired: - if uuid not in current: - parent = session.get(model.Asset, uuid) - assert parent - asset.parents.append(parent) - - for uuid in current: - if uuid not in desired: - parent = session.get(model.Asset, uuid) - assert parent - asset.parents.remove(parent) - return asset def get_asset_type(self): @@ -360,38 +322,6 @@ class AssetMasterView(WuttaFarmMasterView): return buttons - def get_template_context(self, context): - context = super().get_template_context(context) - - if self.viewing: - asset = context["instance"] - - # add location geometry if applicable - if asset.is_fixed and asset.farmos_uuid and not self.app.is_standalone(): - - # TODO: eventually sync GIS data, avoid this API call? - client = get_farmos_client_for_user(self.request) - result = client.asset.get_id(asset.asset_type, asset.farmos_uuid) - if geometry := result["data"]["attributes"]["intrinsic_geometry"]: - - context["map_center"] = [geometry["lon"], geometry["lat"]] - - context["map_bounds"] = [ - [geometry["left"], geometry["bottom"]], - [geometry["right"], geometry["top"]], - ] - - if match := re.match( - r"^POLYGON \(\((?P[^\)]+)\)\)$", geometry["value"] - ): - points = match.group("points").split(", ") - points = [ - [float(pt) for pt in pair.split(" ")] for pair in points - ] - context["map_polygon"] = [points] - - return context - def get_version_joins(self): """ We override this to declare the relationship between the @@ -443,23 +373,6 @@ class AssetMasterView(WuttaFarmMasterView): g.set_filter("log_type", model.Log.log_type) g.set_enum("log_type", get_log_type_enum(self.config, session=session)) - # assets - g.set_renderer("assets", self.render_assets_for_grid) - - def render_assets_for_grid(self, log, field, value): - assets = getattr(log, field) - - if self.farmos_style_grid_links: - links = [] - for asset in assets: - url = self.request.route_url( - f"{asset.asset_type}_assets.view", uuid=asset.uuid - ) - links.append(tags.link_to(str(asset), url)) - return ", ".join(links) - - return ", ".join([str(a) for a in assets]) - def get_row_action_url_view(self, log, i): return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 445f810..674d76e 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -95,8 +95,6 @@ class CommonView(base.CommonView): "farmos_quantities_standard.view", "farmos_quantity_types.list", "farmos_quantity_types.view", - "farmos_seasons.list", - "farmos_seasons.view", "farmos_structure_assets.list", "farmos_structure_assets.view", "farmos_structure_types.list", @@ -134,12 +132,6 @@ class CommonView(base.CommonView): "logs_observation.view", "logs_observation.versions", "quick.eggs", - "plant_types.list", - "plant_types.view", - "plant_types.versions", - "seasons.list", - "seasons.view", - "seasons.versions", "structure_types.list", "structure_types.view", "structure_types.versions", diff --git a/src/wuttafarm/web/views/equipment.py b/src/wuttafarm/web/views/equipment.py deleted file mode 100644 index 8076556..0000000 --- a/src/wuttafarm/web/views/equipment.py +++ /dev/null @@ -1,122 +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 . -# -################################################################################ -""" -Master view for Plants -""" - -from wuttafarm.db.model import EquipmentType, EquipmentAsset -from wuttafarm.web.views import TaxonomyMasterView -from wuttafarm.web.views.assets import AssetMasterView -from wuttafarm.web.forms.schema import EquipmentTypeRefs - - -class EquipmentTypeView(TaxonomyMasterView): - """ - Master view for Equipment Types - """ - - model_class = EquipmentType - route_prefix = "equipment_types" - url_prefix = "/equipment-types" - - farmos_route_prefix = "farmos_equipment_types" - farmos_entity_type = "taxonomy_term" - farmos_bundle = "equipment_type" - farmos_refurl_path = "/admin/structure/taxonomy/manage/equipment_type/overview" - - -class EquipmentAssetView(AssetMasterView): - """ - Master view for Equipment Assets - """ - - model_class = EquipmentAsset - route_prefix = "equipment_assets" - url_prefix = "/assets/equipment" - - farmos_bundle = "equipment" - farmos_refurl_path = "/assets/equipment" - - labels = { - "equipment_types": "Equipment Type", - } - - def configure_form(self, form): - f = form - super().configure_form(f) - equipment = f.model_instance - - # equipment_types - f.fields.insert_after("asset_name", "equipment_types") - f.set_node("equipment_types", EquipmentTypeRefs(self.request)) - if not self.creating: - # nb. must explcitly declare value for non-standard field - f.set_default("equipment_types", equipment.equipment_types) - - # manufacturer - f.fields.insert_after("equipment_types", "manufacturer") - - # model - f.fields.insert_after("manufacturer", "model") - - # serial_number - f.fields.insert_after("model", "serial_number") - - def objectify(self, form): - equipment = super().objectify(form) - data = form.validated - - self.set_equipment_types(equipment, data["equipment_types"]) - - return equipment - - def set_equipment_types(self, equipment, desired): - model = self.app.model - session = self.Session() - current = [str(etype.uuid) for etype in equipment.equipment_types] - - for etype in desired: - if etype["uuid"] not in current: - equipment_type = session.get(model.EquipmentType, etype["uuid"]) - assert equipment_type - equipment.equipment_types.append(equipment_type) - - desired = [etype["uuid"] for etype in desired] - for uuid in current: - if uuid not in desired: - equipment_type = session.get(model.EquipmentType, uuid) - assert equipment_type - equipment.equipment_types.remove(equipment_type) - - -def defaults(config, **kwargs): - base = globals() - - EquipmentTypeView = kwargs.get("EquipmentTypeView", base["EquipmentTypeView"]) - EquipmentTypeView.defaults(config) - - EquipmentAssetView = kwargs.get("EquipmentAssetView", base["EquipmentAssetView"]) - EquipmentAssetView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py index 708b553..e59ac1f 100644 --- a/src/wuttafarm/web/views/farmos/__init__.py +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -28,7 +28,6 @@ from .master import FarmOSMasterView def includeme(config): config.include("wuttafarm.web.views.farmos.users") - config.include("wuttafarm.web.views.farmos.materials") config.include("wuttafarm.web.views.farmos.quantities") config.include("wuttafarm.web.views.farmos.asset_types") config.include("wuttafarm.web.views.farmos.units") @@ -36,15 +35,12 @@ def includeme(config): config.include("wuttafarm.web.views.farmos.land_assets") config.include("wuttafarm.web.views.farmos.structure_types") config.include("wuttafarm.web.views.farmos.structures") - config.include("wuttafarm.web.views.farmos.equipment") config.include("wuttafarm.web.views.farmos.animal_types") config.include("wuttafarm.web.views.farmos.animals") config.include("wuttafarm.web.views.farmos.groups") config.include("wuttafarm.web.views.farmos.plants") - config.include("wuttafarm.web.views.farmos.water") config.include("wuttafarm.web.views.farmos.log_types") config.include("wuttafarm.web.views.farmos.logs_activity") config.include("wuttafarm.web.views.farmos.logs_harvest") config.include("wuttafarm.web.views.farmos.logs_medical") config.include("wuttafarm.web.views.farmos.logs_observation") - config.include("wuttafarm.web.views.farmos.logs_seeding") diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py index 6a6bc92..c99cc5a 100644 --- a/src/wuttafarm/web/views/farmos/animals.py +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -39,7 +39,7 @@ from wuttafarm.web.grids import ( NullableBooleanFilter, DateTimeFilter, ) -from wuttafarm.web.forms.schema import FarmOSRef +from wuttafarm.web.forms.schema import FarmOSRef, FarmOSAssetRefs class AnimalView(AssetMasterView): @@ -99,7 +99,8 @@ class AnimalView(AssetMasterView): def get_farmos_api_includes(self): includes = super().get_farmos_api_includes() - includes.update(["animal_type"]) + includes.add("animal_type") + includes.add("group") return includes def configure_grid(self, grid): @@ -130,6 +131,10 @@ class AnimalView(AssetMasterView): g.set_sorter("sex", SimpleSorter("sex")) g.set_filter("sex", StringFilter) + # groups + g.set_label("groups", "Group Membership") + g.set_renderer("groups", self.render_groups_for_grid) + # is_sterile g.set_renderer("is_sterile", "boolean") g.set_sorter("is_sterile", SimpleSorter("is_sterile")) @@ -140,6 +145,18 @@ class AnimalView(AssetMasterView): url = self.request.route_url("farmos_animal_types.view", uuid=uuid) return tags.link_to(value, url) + def render_groups_for_grid(self, animal, field, value): + groups = [] + for group in animal["groups"]: + if self.farmos_style_grid_links: + url = self.request.route_url( + "farmos_group_assets.view", uuid=group["uuid"] + ) + groups.append(tags.link_to(group["name"], url)) + else: + groups.append(group["name"]) + return ", ".join(groups) + def get_instance(self): data = super().get_instance() @@ -175,6 +192,8 @@ class AnimalView(AssetMasterView): sterile = animal["attributes"]["is_castrated"] animal_type_object = None + group_objects = [] + group_names = [] if relationships := animal.get("relationships"): if animal_type := relationships.get("animal_type"): @@ -184,11 +203,24 @@ class AnimalView(AssetMasterView): "name": animal_type["attributes"]["name"], } + if groups := relationships.get("group"): + for group in groups["data"]: + if group := included.get(group["id"]): + group = { + "uuid": group["id"], + "name": group["attributes"]["name"], + "asset_type": "group", + } + group_objects.append(group) + group_names.append(group["name"]) + normal.update( { "animal_type": animal_type_object, "animal_type_uuid": animal_type_object["uuid"], "animal_type_name": animal_type_object["name"], + "groups": group_objects, + "group_names": group_names, "birthdate": birthdate, "sex": animal["attributes"]["sex"] or colander.null, "is_sterile": sterile, @@ -239,6 +271,12 @@ class AnimalView(AssetMasterView): # is_sterile f.set_node("is_sterile", colander.Boolean()) + # groups + if self.creating or self.editing: + f.remove("groups") # TODO + else: + f.set_node("groups", FarmOSAssetRefs(self.request)) + def get_api_payload(self, animal): payload = super().get_api_payload(animal) diff --git a/src/wuttafarm/web/views/farmos/assets.py b/src/wuttafarm/web/views/farmos/assets.py index 24dd145..11f744b 100644 --- a/src/wuttafarm/web/views/farmos/assets.py +++ b/src/wuttafarm/web/views/farmos/assets.py @@ -24,11 +24,10 @@ Base class for Asset master views """ import colander -import requests from webhelpers2.html import tags from wuttafarm.web.views.farmos import FarmOSMasterView -from wuttafarm.web.forms.schema import FarmOSRefs, FarmOSAssetRefs, FarmOSLocationRefs +from wuttafarm.web.forms.schema import FarmOSRefs, FarmOSLocationRefs from wuttafarm.web.forms.widgets import ImageWidget from wuttafarm.web.grids import ( ResourceData, @@ -76,23 +75,6 @@ class AssetMasterView(FarmOSMasterView): "archived": {"active": True, "verb": "is_false"}, } - form_fields = [ - "name", - "notes", - "asset_type_name", - "owners", - "is_location", - "is_fixed", - "locations", - "groups", - "archived", - "drupal_id", - "thumbnail_url", - "image_url", - "thumbnail", - "image", - ] - def get_grid_data(self, **kwargs): return ResourceData( self.config, @@ -128,10 +110,6 @@ class AssetMasterView(FarmOSMasterView): # locations g.set_renderer("locations", self.render_locations_for_grid) - # groups - g.set_label("groups", "Group Membership") - g.set_renderer("groups", self.render_assets_for_grid) - # archived g.set_renderer("archived", "boolean") g.set_sorter("archived", SimpleSorter("archived")) @@ -142,20 +120,6 @@ class AssetMasterView(FarmOSMasterView): return tags.image(url, f"thumbnail for {self.get_model_title()}") return None - def render_assets_for_grid(self, log, field, value): - if not value: - return "" - - assets = [] - for asset in value: - if self.farmos_style_grid_links: - route = f"farmos_{asset['asset_type']}_assets.view" - url = self.request.route_url(route, uuid=asset["uuid"]) - assets.append(tags.link_to(asset["name"], url)) - else: - assets.append(asset["name"]) - return ", ".join(assets) - def render_locations_for_grid(self, asset, field, value): locations = [] for location in value: @@ -175,19 +139,14 @@ class AssetMasterView(FarmOSMasterView): return None def get_farmos_api_includes(self): - return {"asset_type", "location", "group", "owner", "image"} + return {"asset_type", "location", "owner", "image"} def get_instance(self): - try: - result = self.farmos_client.asset.get_id( - self.farmos_asset_type, - self.request.matchdict["uuid"], - params={"include": ",".join(self.get_farmos_api_includes())}, - ) - except requests.HTTPError as exc: - if exc.response.status_code == 404: - raise self.notfound() - raise + result = self.farmos_client.asset.get_id( + self.farmos_asset_type, + self.request.matchdict["uuid"], + params={"include": ",".join(self.get_farmos_api_includes())}, + ) self.raw_json = result included = {obj["id"]: obj for obj in result.get("included", [])} return self.normalize_asset(result["data"], included) @@ -211,7 +170,6 @@ class AssetMasterView(FarmOSMasterView): owner_names = [] location_objects = [] location_names = [] - group_objects = [] thumbnail_url = None image_url = None if relationships := asset.get("relationships"): @@ -245,16 +203,6 @@ class AssetMasterView(FarmOSMasterView): location_objects.append(location) location_names.append(location["name"]) - if groups := relationships.get("group"): - for group in groups["data"]: - if group := included.get(group["id"]): - group = { - "uuid": group["id"], - "name": group["attributes"]["name"], - "asset_type": "group", - } - group_objects.append(group) - if images := relationships.get("image"): for image in images["data"]: if image := included.get(image["id"]): @@ -269,14 +217,11 @@ class AssetMasterView(FarmOSMasterView): "name": asset["attributes"]["name"], "asset_type": asset_type_object, "asset_type_name": asset_type_name, - "is_location": asset["attributes"]["is_location"], - "is_fixed": asset["attributes"]["is_fixed"], "notes": notes or colander.null, "owners": owner_objects, "owner_names": owner_names, "locations": location_objects, "location_names": location_names, - "groups": group_objects, "archived": archived, "thumbnail_url": thumbnail_url or colander.null, "image_url": image_url or colander.null, @@ -298,12 +243,6 @@ class AssetMasterView(FarmOSMasterView): f.set_label("locations", "Current Location") f.set_node("locations", FarmOSLocationRefs(self.request)) - # groups - if self.creating or self.editing: - f.remove("groups") # TODO - else: - f.set_node("groups", FarmOSAssetRefs(self.request)) - # owners if self.creating or self.editing: f.remove("owners") # TODO @@ -314,16 +253,6 @@ class AssetMasterView(FarmOSMasterView): f.set_widget("notes", "notes") f.set_required("notes", False) - # is_location - f.set_node("is_location", colander.Boolean()) - - # is_fixed - f.set_node("is_fixed", colander.Boolean()) - - # groups - if self.creating or self.editing: - f.remove("groups") # TODO: add support for this? - # archived f.set_node("archived", colander.Boolean()) diff --git a/src/wuttafarm/web/views/farmos/equipment.py b/src/wuttafarm/web/views/farmos/equipment.py deleted file mode 100644 index 6855747..0000000 --- a/src/wuttafarm/web/views/farmos/equipment.py +++ /dev/null @@ -1,211 +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 . -# -################################################################################ -""" -Master view for farmOS Equipment -""" - -from webhelpers2.html import tags - -from wuttafarm.web.views.farmos.master import TaxonomyMasterView -from wuttafarm.web.views.farmos.assets import AssetMasterView -from wuttafarm.web.forms.schema import FarmOSEquipmentTypeRefs - - -class EquipmentTypeView(TaxonomyMasterView): - """ - Master view for Equipment Types in farmOS. - """ - - model_name = "farmos_equipment_type" - model_title = "farmOS Equipment Type" - model_title_plural = "farmOS Equipment Types" - - route_prefix = "farmos_equipment_types" - url_prefix = "/farmOS/equipment-types" - - farmos_taxonomy_type = "equipment_type" - farmos_refurl_path = "/admin/structure/taxonomy/manage/equipment_type/overview" - - def get_xref_buttons(self, equipment_type): - buttons = super().get_xref_buttons(equipment_type) - model = self.app.model - session = self.Session() - - if wf_equipment_type := ( - session.query(model.EquipmentType) - .filter(model.EquipmentType.farmos_uuid == equipment_type["uuid"]) - .first() - ): - buttons.append( - self.make_button( - f"View {self.app.get_title()} record", - primary=True, - url=self.request.route_url( - "equipment_types.view", uuid=wf_equipment_type.uuid - ), - icon_left="eye", - ) - ) - - return buttons - - -class EquipmentAssetView(AssetMasterView): - """ - Master view for farmOS Equipment Assets - """ - - model_name = "farmos_equipment_assets" - model_title = "farmOS Equipment Asset" - model_title_plural = "farmOS Equipment Assets" - - route_prefix = "farmos_equipment_assets" - url_prefix = "/farmOS/assets/equipment" - - farmos_asset_type = "equipment" - farmos_refurl_path = "/assets/equipment" - - labels = { - "equipment_types": "Equipment Type", - } - - grid_columns = [ - "thumbnail", - "drupal_id", - "name", - "equipment_types", - "manufacturer", - "model", - "serial_number", - "groups", - "owners", - "archived", - ] - - def get_farmos_api_includes(self): - includes = super().get_farmos_api_includes() - includes.update(["equipment_type"]) - return includes - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # equipment_types - g.set_renderer("equipment_types", self.render_equipment_types_for_grid) - - def render_equipment_types_for_grid(self, equipment, field, value): - if self.farmos_style_grid_links: - links = [] - for etype in value: - url = self.request.route_url( - f"farmos_equipment_types.view", uuid=etype["uuid"] - ) - links.append(tags.link_to(etype["name"], url)) - return ", ".join(links) - - return ", ".join([etype["name"] for etype in value]) - - def normalize_asset(self, equipment, included): - data = super().normalize_asset(equipment, included) - - equipment_type_objects = [] - rels = equipment["relationships"] - for etype in rels["equipment_type"]["data"]: - uuid = etype["id"] - equipment_type = { - "uuid": uuid, - "type": etype["type"], - } - if etype := included.get(uuid): - equipment_type.update( - { - "name": etype["attributes"]["name"], - } - ) - equipment_type_objects.append(equipment_type) - - data.update( - { - "manufacturer": equipment["attributes"]["manufacturer"], - "model": equipment["attributes"]["model"], - "serial_number": equipment["attributes"]["serial_number"], - "equipment_types": equipment_type_objects, - } - ) - - return data - - def configure_form(self, form): - f = form - super().configure_form(f) - - # equipment_types - f.fields.insert_after("name", "equipment_types") - f.set_node("equipment_types", FarmOSEquipmentTypeRefs(self.request)) - - # manufacturer - f.fields.insert_after("equipment_types", "manufacturer") - - # model - f.fields.insert_after("manufacturer", "model") - - # serial_number - f.fields.insert_after("model", "serial_number") - - def get_xref_buttons(self, equipment): - model = self.app.model - session = self.Session() - - buttons = super().get_xref_buttons(equipment) - - if wf_equipment := ( - session.query(model.Asset) - .filter(model.Asset.farmos_uuid == equipment["uuid"]) - .first() - ): - buttons.append( - self.make_button( - f"View {self.app.get_title()} record", - primary=True, - url=self.request.route_url( - "equipment_assets.view", uuid=wf_equipment.uuid - ), - icon_left="eye", - ) - ) - - return buttons - - -def defaults(config, **kwargs): - base = globals() - - EquipmentTypeView = kwargs.get("EquipmentTypeView", base["EquipmentTypeView"]) - EquipmentTypeView.defaults(config) - - EquipmentAssetView = kwargs.get("EquipmentAssetView", base["EquipmentAssetView"]) - EquipmentAssetView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/farmos/logs.py b/src/wuttafarm/web/views/farmos/logs.py index 74fb941..cb7a87b 100644 --- a/src/wuttafarm/web/views/farmos/logs.py +++ b/src/wuttafarm/web/views/farmos/logs.py @@ -45,7 +45,7 @@ from wuttafarm.web.forms.schema import ( LogQuick, Notes, ) -from wuttafarm.web.util import render_quantity_objects, render_quantity_object +from wuttafarm.web.util import render_quantity_objects class LogMasterView(FarmOSMasterView): @@ -199,20 +199,7 @@ class LogMasterView(FarmOSMasterView): ) self.raw_json = result included = {obj["id"]: obj for obj in result.get("included", [])} - instance = self.normalize_log(result["data"], included) - - for qty in instance["quantities"]: - - if qty["quantity_type_id"] == "material": - for mtype in qty["material_types"]: - result = self.farmos_client.resource.get_id( - "taxonomy_term", "material_type", mtype["uuid"] - ) - mtype["name"] = result["data"]["attributes"]["name"] - - qty["as_text"] = render_quantity_object(qty) - - return instance + return self.normalize_log(result["data"], included) def get_instance_title(self, log): return log["name"] diff --git a/src/wuttafarm/web/views/farmos/logs_seeding.py b/src/wuttafarm/web/views/farmos/logs_seeding.py deleted file mode 100644 index ed967cc..0000000 --- a/src/wuttafarm/web/views/farmos/logs_seeding.py +++ /dev/null @@ -1,105 +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 . -# -################################################################################ -""" -View for farmOS Seeding Logs -""" - -from wuttaweb.forms.schema import WuttaDateTime -from wuttaweb.forms.widgets import WuttaDateTimeWidget - -from wuttafarm.web.views.farmos.logs import LogMasterView -from wuttafarm.web.grids import SimpleSorter, StringFilter - - -class SeedingLogView(LogMasterView): - """ - View for farmOS seeding logs - """ - - model_name = "farmos_seeding_log" - model_title = "farmOS Seeding Log" - model_title_plural = "farmOS Seeding Logs" - - route_prefix = "farmos_logs_seeding" - url_prefix = "/farmOS/logs/seeding" - - farmos_log_type = "seeding" - farmos_refurl_path = "/logs/seeding" - - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "name", - "assets", - "locations", - "purchase_date", - "source", - "is_group_assignment", - "owners", - ] - - def normalize_log(self, log, included): - data = super().normalize_log(log, included) - data.update( - { - "source": log["attributes"]["source"], - "purchase_date": self.normal.normalize_datetime( - log["attributes"]["purchase_date"] - ), - "lot_number": log["attributes"]["lot_number"], - } - ) - return data - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # purchase_date - g.set_renderer("purchase_date", "date") - - def configure_form(self, form): - f = form - super().configure_form(f) - - # source - f.fields.insert_after("timestamp", "source") - - # purchase_date - f.fields.insert_after("source", "purchase_date") - f.set_node("purchase_date", WuttaDateTime()) - f.set_widget("purchase_date", WuttaDateTimeWidget(self.request)) - - # lot_number - f.fields.insert_after("purchase_date", "lot_number") - - -def defaults(config, **kwargs): - base = globals() - - SeedingLogView = kwargs.get("SeedingLogView", base["SeedingLogView"]) - SeedingLogView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/farmos/materials.py b/src/wuttafarm/web/views/farmos/materials.py deleted file mode 100644 index e56557d..0000000 --- a/src/wuttafarm/web/views/farmos/materials.py +++ /dev/null @@ -1,76 +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 . -# -################################################################################ -""" -View for farmOS materials -""" - -from wuttafarm.web.views.farmos.master import TaxonomyMasterView - - -class MaterialTypeView(TaxonomyMasterView): - """ - Master view for Material Types in farmOS. - """ - - model_name = "farmos_material_type" - model_title = "farmOS Material Type" - model_title_plural = "farmOS Material Types" - - route_prefix = "farmos_material_types" - url_prefix = "/farmOS/material-types" - - farmos_taxonomy_type = "material_type" - farmos_refurl_path = "/admin/structure/taxonomy/manage/material_type/overview" - - def get_xref_buttons(self, material_type): - buttons = super().get_xref_buttons(material_type) - model = self.app.model - session = self.Session() - - if wf_material_type := ( - session.query(model.MaterialType) - .filter(model.MaterialType.farmos_uuid == material_type["uuid"]) - .first() - ): - buttons.append( - self.make_button( - f"View {self.app.get_title()} record", - primary=True, - url=self.request.route_url( - "material_types.view", uuid=wf_material_type.uuid - ), - icon_left="eye", - ) - ) - - return buttons - - -def defaults(config, **kwargs): - base = globals() - - MaterialTypeView = kwargs.get("MaterialTypeView", base["MaterialTypeView"]) - MaterialTypeView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/farmos/plants.py b/src/wuttafarm/web/views/farmos/plants.py index 40768c4..57bf2d4 100644 --- a/src/wuttafarm/web/views/farmos/plants.py +++ b/src/wuttafarm/web/views/farmos/plants.py @@ -32,12 +32,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget from wuttafarm.web.views.farmos.master import TaxonomyMasterView from wuttafarm.web.views.farmos import FarmOSMasterView -from wuttafarm.web.forms.schema import ( - UsersType, - StructureType, - FarmOSPlantTypes, - FarmOSRefs, -) +from wuttafarm.web.forms.schema import UsersType, StructureType, FarmOSPlantTypes from wuttafarm.web.forms.widgets import ImageWidget @@ -80,43 +75,6 @@ class PlantTypeView(TaxonomyMasterView): 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): """ Master view for farmOS Plant Assets @@ -131,10 +89,6 @@ class PlantAssetView(FarmOSMasterView): farmos_refurl_path = "/assets/plant" - labels = { - "seasons": "Season", - } - grid_columns = [ "name", "archived", @@ -145,7 +99,6 @@ class PlantAssetView(FarmOSMasterView): form_fields = [ "name", "plant_types", - "seasons", "archived", "owners", "location", @@ -198,21 +151,6 @@ 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 if location := relationships.get("location"): if location["data"]: @@ -261,14 +199,22 @@ class PlantAssetView(FarmOSMasterView): return plant["name"] 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 { - "uuid": normal["uuid"], - "drupal_id": normal["drupal_id"], - "name": normal["asset_name"], + "uuid": plant["id"], + "drupal_id": plant["attributes"]["drupal_internal__id"], + "name": plant["attributes"]["name"], "location": colander.null, # TODO - "archived": normal["archived"], - "notes": normal["notes"] or colander.null, + "archived": archived, + "notes": notes or colander.null, } def configure_form(self, form): @@ -279,9 +225,6 @@ class PlantAssetView(FarmOSMasterView): # plant_types f.set_node("plant_types", FarmOSPlantTypes(self.request)) - # seasons - f.set_node("seasons", FarmOSRefs(self.request, "farmos_seasons")) - # location f.set_node("location", StructureType(self.request)) @@ -336,9 +279,6 @@ def defaults(config, **kwargs): PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"]) PlantTypeView.defaults(config) - SeasonView = kwargs.get("SeasonView", base["SeasonView"]) - SeasonView.defaults(config) - PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"]) PlantAssetView.defaults(config) diff --git a/src/wuttafarm/web/views/farmos/quantities.py b/src/wuttafarm/web/views/farmos/quantities.py index bd2e519..a388559 100644 --- a/src/wuttafarm/web/views/farmos/quantities.py +++ b/src/wuttafarm/web/views/farmos/quantities.py @@ -26,13 +26,12 @@ View for farmOS Quantity Types import datetime import colander -import requests 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 FarmOSUnitRef, FarmOSRefs +from wuttafarm.web.forms.schema import FarmOSUnitRef from wuttafarm.web.grids import ResourceData @@ -143,7 +142,6 @@ class QuantityMasterView(FarmOSMasterView): sort_defaults = ("drupal_id", "desc") form_fields = [ - "quantity_type_name", "measure", "value", "units", @@ -173,7 +171,6 @@ class QuantityMasterView(FarmOSMasterView): # as_text g.set_renderer("as_text", self.render_as_text_for_grid) - g.set_link("as_text") # measure g.set_renderer("measure", self.render_measure_for_grid) @@ -206,26 +203,14 @@ class QuantityMasterView(FarmOSMasterView): return qty["value"]["decimal"] def get_instance(self): - # TODO: this pattern should be repeated for other views - try: - result = self.farmos_client.resource.get_id( - "quantity", - self.farmos_quantity_type, - self.request.matchdict["uuid"], - params={"include": self.get_farmos_api_includes()}, - ) - except requests.HTTPError as exc: - if exc.response.status_code == 404: - raise self.notfound() - raise + quantity = self.farmos_client.resource.get_id( + "quantity", self.farmos_quantity_type, self.request.matchdict["uuid"] + ) + self.raw_json = quantity - self.raw_json = result + data = self.normalize_quantity(quantity["data"]) - included = {obj["id"]: obj for obj in result.get("included", [])} - assert included - data = self.normalize_quantity(result["data"], included) - - if relationships := result["data"].get("relationships"): + if relationships := quantity["data"].get("relationships"): # add units if units := relationships.get("units"): @@ -293,11 +278,6 @@ class QuantityMasterView(FarmOSMasterView): f = form super().configure_form(f) - # quantity_type_name - f.set_label("quantity_type_name", "Quantity Type") - f.set_readonly("quantity_type_name") - f.set_default("quantity_type_name", self.farmos_quantity_type.capitalize()) - # created f.set_node("created", WuttaDateTime(self.request)) f.set_widget("created", WuttaDateTimeWidget(self.request)) @@ -323,7 +303,6 @@ class StandardQuantityView(QuantityMasterView): url_prefix = "/farmOS/quantities/standard" farmos_quantity_type = "standard" - farmos_refurl_path = "/log-quantities/standard" def get_xref_buttons(self, standard_quantity): model = self.app.model @@ -350,90 +329,6 @@ class StandardQuantityView(QuantityMasterView): return buttons -class MaterialQuantityView(QuantityMasterView): - """ - View for farmOS Material Quantities - """ - - model_name = "farmos_material_quantity" - model_title = "farmOS Material Quantity" - model_title_plural = "farmOS Material Quantities" - - route_prefix = "farmos_quantities_material" - url_prefix = "/farmOS/quantities/material" - - farmos_quantity_type = "material" - farmos_refurl_path = "/log-quantities/material" - - def get_farmos_api_includes(self): - includes = super().get_farmos_api_includes() - includes.update({"material_type"}) - return includes - - def normalize_quantity(self, quantity, included={}): - normal = super().normalize_quantity(quantity, included) - - material_type_objects = [] - material_type_uuids = [] - if relationships := quantity["relationships"]: - - if material_types := relationships["material_type"]["data"]: - for mtype in material_types: - uuid = mtype["id"] - material_type_uuids.append(uuid) - material_type = { - "uuid": uuid, - "type": mtype["type"], - } - if mtype := included.get(uuid): - material_type.update( - { - "name": mtype["attributes"]["name"], - } - ) - material_type_objects.append(material_type) - - normal.update( - { - "material_types": material_type_objects, - "material_type_uuids": material_type_uuids, - } - ) - return normal - - def configure_form(self, form): - f = form - super().configure_form(f) - - # material_types - f.fields.insert_before("measure", "material_types") - f.set_node("material_types", FarmOSRefs(self.request, "farmos_material_types")) - - def get_xref_buttons(self, material_quantity): - model = self.app.model - session = self.Session() - buttons = [] - - if wf_material_quantity := ( - session.query(model.MaterialQuantity) - .join(model.Quantity) - .filter(model.Quantity.farmos_uuid == material_quantity["uuid"]) - .first() - ): - buttons.append( - self.make_button( - f"View {self.app.get_title()} record", - primary=True, - url=self.request.route_url( - "quantities_material.view", uuid=wf_material_quantity.uuid - ), - icon_left="eye", - ) - ) - - return buttons - - def defaults(config, **kwargs): base = globals() @@ -445,11 +340,6 @@ def defaults(config, **kwargs): ) StandardQuantityView.defaults(config) - MaterialQuantityView = kwargs.get( - "MaterialQuantityView", base["MaterialQuantityView"] - ) - MaterialQuantityView.defaults(config) - def includeme(config): defaults(config) diff --git a/src/wuttafarm/web/views/farmos/water.py b/src/wuttafarm/web/views/farmos/water.py deleted file mode 100644 index 129f22e..0000000 --- a/src/wuttafarm/web/views/farmos/water.py +++ /dev/null @@ -1,81 +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 . -# -################################################################################ -""" -Master view for Water Assets in farmOS -""" - -from wuttafarm.web.views.farmos.assets import AssetMasterView - - -class WaterAssetView(AssetMasterView): - """ - Master view for farmOS Water Assets - """ - - model_name = "farmos_water_assets" - model_title = "farmOS Water Asset" - model_title_plural = "farmOS Water Assets" - - route_prefix = "farmos_water_assets" - url_prefix = "/farmOS/assets/water" - - farmos_asset_type = "water" - farmos_refurl_path = "/assets/water" - - grid_columns = [ - "thumbnail", - "drupal_id", - "name", - "archived", - ] - - def get_xref_buttons(self, water): - model = self.app.model - session = self.Session() - buttons = super().get_xref_buttons(water) - - if wf_water := ( - session.query(model.Asset) - .filter(model.Asset.farmos_uuid == water["uuid"]) - .first() - ): - buttons.append( - self.make_button( - f"View {self.app.get_title()} record", - primary=True, - url=self.request.route_url("water_assets.view", uuid=wf_water.uuid), - icon_left="eye", - ) - ) - - return buttons - - -def defaults(config, **kwargs): - base = globals() - - WaterAssetView = kwargs.get("WaterAssetView", base["WaterAssetView"]) - WaterAssetView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/groups.py b/src/wuttafarm/web/views/groups.py index c8cc7f7..4331280 100644 --- a/src/wuttafarm/web/views/groups.py +++ b/src/wuttafarm/web/views/groups.py @@ -47,11 +47,15 @@ class GroupView(AssetMasterView): "archived", ] - def configure_form(self, f): - super().configure_form(f) - - # produces_eggs - f.fields.insert_after("asset_type", "produces_eggs") + form_fields = [ + "asset_name", + "notes", + "asset_type", + "produces_eggs", + "archived", + "drupal_id", + "farmos_uuid", + ] def defaults(config, **kwargs): diff --git a/src/wuttafarm/web/views/logs.py b/src/wuttafarm/web/views/logs.py index 2a4e6e0..9c983b7 100644 --- a/src/wuttafarm/web/views/logs.py +++ b/src/wuttafarm/web/views/logs.py @@ -34,7 +34,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.db.model import LogType, Log -from wuttafarm.web.forms.schema import AssetRefs, QuantityRefs, OwnerRefs +from wuttafarm.web.forms.schema import AssetRefs, LogQuantityRefs, OwnerRefs from wuttafarm.util import get_log_type_enum @@ -256,21 +256,26 @@ class LogMasterView(WuttaFarmMasterView): f.set_default("timestamp", self.app.make_utc()) # assets - f.set_node("assets", AssetRefs(self.request)) - f.set_required("assets", False) - if not self.creating: + if self.creating or self.editing: + f.remove("assets") # TODO: need to support this + else: + f.set_node("assets", AssetRefs(self.request)) # nb. must explicity declare value for non-standard field f.set_default("assets", log.assets) # groups - f.set_node("groups", AssetRefs(self.request, is_group=True)) - if not self.creating: + if self.creating or self.editing: + f.remove("groups") # TODO: need to support this + else: + f.set_node("groups", AssetRefs(self.request)) # nb. must explicity declare value for non-standard field f.set_default("groups", log.groups) # locations - f.set_node("locations", AssetRefs(self.request, is_location=True)) - if not self.creating: + if self.creating or self.editing: + f.remove("locations") # TODO: need to support this + else: + f.set_node("locations", AssetRefs(self.request)) # nb. must explicity declare value for non-standard field f.set_default("locations", log.locations) @@ -287,12 +292,12 @@ class LogMasterView(WuttaFarmMasterView): f.set_readonly("log_type") # quantities - f.set_node("quantities", QuantityRefs(self.request)) - if not self.creating: + if self.creating or self.editing: + f.remove("quantities") # TODO: need to support this + else: + f.set_node("quantities", LogQuantityRefs(self.request)) # nb. must explicity declare value for non-standard field - f.set_default( - "quantities", [self.app.get_true_quantity(q) for q in log.quantities] - ) + f.set_default("quantities", log.quantities) # notes f.set_widget("notes", "notes") @@ -319,141 +324,13 @@ class LogMasterView(WuttaFarmMasterView): def objectify(self, form): log = super().objectify(form) - data = form.validated if self.creating: - - # log_type + model_class = self.get_model_class() log.log_type = self.get_farmos_log_type() - # owner - log.owners = [self.request.user] - - self.set_assets(log, data["assets"]) - self.set_locations(log, data["locations"]) - self.set_groups(log, data["groups"]) - self.set_quantities(log, data["quantities"]) - return log - def set_assets(self, log, desired): - model = self.app.model - session = self.Session() - current = [a.uuid for a in log.assets] - - for uuid in desired: - if uuid not in current: - asset = session.get(model.Asset, uuid) - assert asset - log.assets.append(asset) - - for uuid in current: - if uuid not in desired: - asset = session.get(model.Asset, uuid) - assert asset - log.assets.remove(asset) - - def set_locations(self, log, desired): - model = self.app.model - session = self.Session() - current = [l.uuid for l in log.locations] - - for uuid in desired: - if uuid not in current: - location = session.get(model.Asset, uuid) - assert location - log.locations.append(location) - - for uuid in current: - if uuid not in desired: - location = session.get(model.Asset, uuid) - assert location - log.locations.remove(location) - - def set_groups(self, log, desired): - model = self.app.model - session = self.Session() - current = [g.uuid for g in log.groups] - - for uuid in desired: - if uuid not in current: - group = session.get(model.Asset, uuid) - assert group - log.groups.append(group) - - for uuid in current: - if uuid not in desired: - group = session.get(model.Asset, uuid) - assert group - log.groups.remove(group) - - def set_quantities(self, log, desired): - model = self.app.model - session = self.Session() - - current = { - qty.uuid.hex: self.app.get_true_quantity(qty) for qty in log.quantities - } - for new_qty in desired: - units = session.get(model.Unit, new_qty["units"]["uuid"]) - assert units - if new_qty["uuid"].startswith("new_"): - qty = self.app.make_true_quantity( - new_qty["quantity_type"]["drupal_id"], - measure_id=new_qty["measure"], - value_numerator=int(new_qty["value"]), - value_denominator=1, - units=units, - ) - # nb. must ensure "typed" quantity record persists! - session.add(qty) - # but must add "generic" quantity record to log - log.quantities.append(qty.quantity) - else: - old_qty = current[new_qty["uuid"]] - old_qty.measure_id = new_qty["measure"] - old_qty.value_numerator = int(new_qty["value"]) - old_qty.value_denominator = 1 - old_qty.units = units - if old_qty.quantity_type_id == "material": - self.set_material_types(old_qty, new_qty["material_types"]) - - desired = [qty["uuid"] for qty in desired] - for old_qty in list(log.quantities): - # nb. "old_qty" may be newly-created, w/ no uuid yet - # (this logic may break if session gets flushed early!) - if old_qty.uuid and old_qty.uuid.hex not in desired: - log.quantities.remove(old_qty) - - def set_material_types(self, quantity, desired): - model = self.app.model - session = self.Session() - current = {mtype.uuid: mtype for mtype in quantity.material_types} - - for new_mtype in desired: - mtype = session.get(model.MaterialType, new_mtype["uuid"]) - assert mtype - if mtype.uuid not in current: - quantity.material_types.append(mtype) - - desired = [mtype["uuid"] for mtype in desired] - for old_mtype in current.values(): - if old_mtype.uuid.hex not in desired: - quantity.material_types.remove(old_mtype) - - def auto_sync_to_farmos(self, client, log): - model = self.app.model - session = self.Session() - - # nb. ensure quantities have uuid keys - session.flush() - - for qty in log.quantities: - qty = self.app.get_true_quantity(qty) - self.app.auto_sync_to_farmos(qty, client=client) - - self.app.auto_sync_to_farmos(log, client=client) - def get_farmos_url(self, log): return self.app.get_farmos_url(f"/log/{log.drupal_id}") @@ -491,7 +368,6 @@ class LogMasterView(WuttaFarmMasterView): return super().get_version_joins() + [ model.Log, (model.LogAsset, "log_uuid", "uuid"), - (model.LogQuantity, "log_uuid", "uuid"), ] diff --git a/src/wuttafarm/web/views/logs_seeding.py b/src/wuttafarm/web/views/logs_seeding.py deleted file mode 100644 index 8946aff..0000000 --- a/src/wuttafarm/web/views/logs_seeding.py +++ /dev/null @@ -1,80 +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 . -# -################################################################################ -""" -Master view for Seeding Logs -""" - -from wuttaweb.forms.widgets import WuttaDateTimeWidget - -from wuttafarm.web.views.logs import LogMasterView -from wuttafarm.db.model import SeedingLog - - -class SeedingLogView(LogMasterView): - """ - Master view for Seeding Logs - """ - - model_class = SeedingLog - route_prefix = "logs_seeding" - url_prefix = "/logs/seeding" - - farmos_bundle = "seeding" - farmos_refurl_path = "/logs/seeding" - - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "message", - "assets", - "locations", - "purchase_date", - "source", - "is_group_assignment", - "owners", - ] - - def configure_form(self, form): - f = form - super().configure_form(f) - - # source - f.fields.insert_after("timestamp", "source") - - # purchase_date - f.fields.insert_after("source", "purchase_date") - f.set_widget("purchase_date", WuttaDateTimeWidget(self.request)) - - # lot_number - f.fields.insert_after("purchase_date", "lot_number") - - -def defaults(config, **kwargs): - base = globals() - - SeedingLogView = kwargs.get("SeedingLogView", base["SeedingLogView"]) - SeedingLogView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/master.py b/src/wuttafarm/web/views/master.py index c828b96..747cdc5 100644 --- a/src/wuttafarm/web/views/master.py +++ b/src/wuttafarm/web/views/master.py @@ -23,14 +23,9 @@ Base class for WuttaFarm master views """ -import threading -import time - -import requests from webhelpers2.html import tags from wuttaweb.views import MasterView -from wuttaweb.util import get_form_data from wuttafarm.web.util import use_farmos_style_grid_links, get_farmos_client_for_user @@ -111,35 +106,13 @@ class WuttaFarmMasterView(MasterView): f.set_readonly("drupal_id") def persist(self, obj, session=None): - session = session or self.Session() # save per usual super().persist(obj, session) # maybe also sync change to farmOS if self.app.is_farmos_mirror(): - if self.creating: - session.flush() # need the new uuid client = get_farmos_client_for_user(self.request) - thread = threading.Thread( - target=self.auto_sync_to_farmos, args=(client, obj.uuid) - ) - thread.start() - - def auto_sync_to_farmos(self, client, uuid): - model = self.app.model - model_class = self.get_model_class() - - with self.app.short_session(commit=True) as session: - if user := session.query(model.User).filter_by(username="farmos").first(): - session.info["continuum_user_id"] = user.uuid - - obj = None - while not obj: - obj = session.get(model_class, uuid) - if not obj: - time.sleep(0.1) - self.app.auto_sync_to_farmos(obj, client=client, require=False) def get_farmos_entity_type(self): @@ -168,130 +141,7 @@ class WuttaFarmMasterView(MasterView): # maybe delete from farmOS also if farmos_uuid: + entity_type = self.get_farmos_entity_type() + bundle = self.get_farmos_bundle() client = get_farmos_client_for_user(self.request) - # nb. must use separate thread to avoid some kind of race - # condition (?) - seems as though maybe a "boomerang" - # effect is happening; this seems to help anyway - thread = threading.Thread( - target=self.delete_from_farmos, args=(client, farmos_uuid) - ) - thread.start() - - def delete_from_farmos(self, client, farmos_uuid): - entity_type = self.get_farmos_entity_type() - bundle = self.get_farmos_bundle() - try: client.resource.delete(entity_type, bundle, farmos_uuid) - except requests.HTTPError as exc: - # ignore if record not found in farmOS - if exc.response.status_code != 404: - raise - - -class TaxonomyMasterView(WuttaFarmMasterView): - """ - Base class for master views serving taxonomy terms. - """ - - farmos_entity_type = "taxonomy_term" - - grid_columns = [ - "name", - "description", - ] - - sort_defaults = "name" - - filter_defaults = { - "name": {"active": True, "verb": "contains"}, - } - - form_fields = [ - "name", - "description", - "drupal_id", - "farmos_uuid", - ] - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # name - g.set_link("name") - - def configure_form(self, form): - f = form - super().configure_form(f) - - # description - f.set_widget("description", "notes") - - def get_farmos_url(self, obj): - return self.app.get_farmos_url(f"/taxonomy/term/{obj.drupal_id}") - - def get_xref_buttons(self, term): - buttons = super().get_xref_buttons(term) - - if term.farmos_uuid: - buttons.append( - self.make_button( - "View farmOS record", - primary=True, - url=self.request.route_url( - f"{self.farmos_route_prefix}.view", uuid=term.farmos_uuid - ), - icon_left="eye", - ) - ) - - return buttons - - def ajax_create(self): - """ - AJAX view to create a new taxonomy term. - """ - 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"} - - term = self.model_class(name=name) - session.add(term) - session.flush() - - if self.app.is_farmos_mirror(): - client = get_farmos_client_for_user(self.request) - self.app.auto_sync_to_farmos(term, client=client) - - return { - "uuid": term.uuid.hex, - "name": term.name, - "farmos_uuid": term.farmos_uuid.hex, - "drupal_id": term.drupal_id, - } - - @classmethod - def defaults(cls, config): - """ """ - cls._defaults(config) - cls._taxonomy_defaults(config) - - @classmethod - def _taxonomy_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", - ) diff --git a/src/wuttafarm/web/views/material_types.py b/src/wuttafarm/web/views/material_types.py deleted file mode 100644 index d2118a7..0000000 --- a/src/wuttafarm/web/views/material_types.py +++ /dev/null @@ -1,52 +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 . -# -################################################################################ -""" -Master view for Material Types -""" - -from wuttafarm.web.views import TaxonomyMasterView -from wuttafarm.db.model import MaterialType - - -class MaterialTypeView(TaxonomyMasterView): - """ - Master view for Material Types - """ - - model_class = MaterialType - route_prefix = "material_types" - url_prefix = "/material-types" - - farmos_route_prefix = "farmos_material_types" - farmos_bundle = "material_type" - farmos_refurl_path = "/admin/structure/taxonomy/manage/material_type/overview" - - -def defaults(config, **kwargs): - base = globals() - - MaterialTypeView = kwargs.get("MaterialTypeView", base["MaterialTypeView"]) - MaterialTypeView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/plants.py b/src/wuttafarm/web/views/plants.py index 16bd3c0..a114e07 100644 --- a/src/wuttafarm/web/views/plants.py +++ b/src/wuttafarm/web/views/plants.py @@ -28,9 +28,9 @@ from webhelpers2.html import tags from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.util import get_form_data -from wuttafarm.db.model import PlantType, Season, PlantAsset +from wuttafarm.db.model import PlantType, PlantAsset from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView -from wuttafarm.web.forms.schema import PlantTypeRefs, SeasonRefs +from wuttafarm.web.forms.schema import PlantTypeRefs from wuttafarm.web.forms.widgets import ImageWidget from wuttafarm.web.util import get_farmos_client_for_user @@ -195,166 +195,6 @@ 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): """ Master view for Plant Assets @@ -369,7 +209,6 @@ class PlantAssetView(AssetMasterView): labels = { "plant_types": "Crop/Variety", - "seasons": "Season", } grid_columns = [ @@ -381,6 +220,21 @@ class PlantAssetView(AssetMasterView): "archived", ] + form_fields = [ + "asset_name", + "plant_types", + "season", + "notes", + "asset_type", + "archived", + "drupal_id", + "farmos_uuid", + "thumbnail_url", + "image_url", + "thumbnail", + "image", + ] + def configure_grid(self, grid): g = grid super().configure_grid(g) @@ -408,33 +262,23 @@ class PlantAssetView(AssetMasterView): plant = f.model_instance # plant_types - f.fields.insert_after("asset_name", "plant_types") f.set_node("plant_types", PlantTypeRefs(self.request)) if not self.creating: # nb. must explcitly declare value for non-standard field f.set_default("plant_types", [pt.uuid for pt in plant.plant_types]) # season - f.fields.insert_after("plant_types", "seasons") - 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) + if self.creating or self.editing: + f.remove("season") # TODO: add support for this def objectify(self, form): + model = self.app.model + session = self.Session() plant = super().objectify(form) 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] + desired = data["plant_types"] for uuid in desired: if uuid not in current: @@ -448,22 +292,7 @@ class PlantAssetView(AssetMasterView): assert plant_type plant.plant_types.remove(plant_type) - 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) + return plant def defaults(config, **kwargs): @@ -472,9 +301,6 @@ def defaults(config, **kwargs): PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"]) PlantTypeView.defaults(config) - SeasonView = kwargs.get("SeasonView", base["SeasonView"]) - SeasonView.defaults(config) - PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"]) PlantAssetView.defaults(config) diff --git a/src/wuttafarm/web/views/quantities.py b/src/wuttafarm/web/views/quantities.py index 9a91941..d4112cf 100644 --- a/src/wuttafarm/web/views/quantities.py +++ b/src/wuttafarm/web/views/quantities.py @@ -25,19 +25,11 @@ Master view for Quantities from collections import OrderedDict -from webhelpers2.html import tags - from wuttaweb.db import Session from wuttafarm.web.views import WuttaFarmMasterView -from wuttafarm.db.model import ( - QuantityType, - Quantity, - StandardQuantity, - MaterialQuantity, -) -from wuttafarm.web.forms.schema import UnitRef, LogRef, MaterialTypeRefs -from wuttafarm.util import get_log_type_enum +from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity +from wuttafarm.web.forms.schema import UnitRef, LogRef def get_quantity_type_enum(config): @@ -108,28 +100,17 @@ class QuantityMasterView(WuttaFarmMasterView): Base class for Quantity master views """ - farmos_entity_type = "quantity" - - labels = { - "log_id": "Log ID", - } - grid_columns = [ "drupal_id", - "log_id", - "log_status", - "log_timestamp", - "log_type", - "log_name", - "log_assets", + "as_text", + "quantity_type", "measure", "value", "units", "label", - "quantity_type", ] - sort_defaults = ("log_timestamp", "desc") + sort_defaults = ("drupal_id", "desc") form_fields = [ "quantity_type", @@ -148,15 +129,10 @@ class QuantityMasterView(WuttaFarmMasterView): 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) - - query = query.outerjoin(model.LogQuantity).outerjoin(model.Log) - return query def configure_grid(self, grid): @@ -164,39 +140,14 @@ class QuantityMasterView(WuttaFarmMasterView): super().configure_grid(g) model = self.app.model model_class = self.get_model_class() - session = self.Session() # drupal_id g.set_label("drupal_id", "ID", column_only=True) g.set_sorter("drupal_id", model.Quantity.drupal_id) - # log_id - g.set_renderer("log_id", self.render_log_id_for_grid) - g.set_sorter("log_id", model.Log.drupal_id) - - # log_status - g.set_renderer("log_status", self.render_log_status_for_grid) - g.set_sorter("log_status", model.Log.status) - - # log_timestamp - g.set_renderer("log_timestamp", self.render_log_timestamp_for_grid) - g.set_sorter("log_timestamp", model.Log.timestamp) - - # log_type - self.log_type_enum = get_log_type_enum(self.config, session) - g.set_renderer("log_type", self.render_log_type_for_grid) - g.set_sorter("log_type", model.Log.log_type) - - # log_name - g.set_renderer("log_name", self.render_log_name_for_grid) - g.set_sorter("log_name", model.Log.message) - if not self.farmos_style_grid_links: - g.set_link("log_name") - - # log_assets - g.set_renderer("log_assets", self.render_log_assets_for_grid) - if not self.farmos_style_grid_links: - g.set_link("log_assets") + # 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: @@ -226,47 +177,8 @@ class QuantityMasterView(WuttaFarmMasterView): g.add_action("view", icon="eye", url=quantity_url) - def render_log_id_for_grid(self, quantity, field, value): - if log := quantity.log: - return log.drupal_id - return None - - def render_log_status_for_grid(self, quantity, field, value): - enum = self.app.enum - if log := quantity.log: - return enum.LOG_STATUS.get(log.status, log.status) - return None - - def render_log_timestamp_for_grid(self, quantity, field, value): - if log := quantity.log: - return self.app.render_date(log.timestamp) - return None - - def render_log_type_for_grid(self, quantity, field, value): - if log := quantity.log: - return self.log_type_enum.get(log.log_type, log.log_type) - return None - - def render_log_name_for_grid(self, quantity, field, value): - if log := quantity.log: - if self.farmos_style_grid_links: - url = self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) - return tags.link_to(log.message, url) - return log.message - return None - - def render_log_assets_for_grid(self, quantity, field, value): - if log := quantity.log: - if self.farmos_style_grid_links: - links = [] - for asset in log.assets: - url = self.request.route_url( - f"{asset.asset_type}_assets.view", uuid=asset.uuid - ) - links.append(tags.link_to(str(asset), url)) - return ", ".join(links) - return [str(a) for a in log.assets] - return None + 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 @@ -359,8 +271,6 @@ class AllQuantityView(QuantityMasterView): deletable = False model_is_versioned = False - farmos_refurl_path = "/log-quantities" - class StandardQuantityView(QuantityMasterView): """ @@ -371,77 +281,6 @@ class StandardQuantityView(QuantityMasterView): route_prefix = "quantities_standard" url_prefix = "/quantities/standard" - farmos_bundle = "standard" - farmos_refurl_path = "/log-quantities/standard" - - -class MaterialQuantityView(QuantityMasterView): - """ - Master view for Material Quantities - """ - - model_class = MaterialQuantity - route_prefix = "quantities_material" - url_prefix = "/quantities/material" - - farmos_bundle = "material" - farmos_refurl_path = "/log-quantities/material" - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # material_types - g.columns.append("material_types") - g.set_label("material_types", "Material Type", column_only=True) - g.set_renderer("material_types", self.render_material_types_for_grid) - - def render_material_types_for_grid(self, quantity, field, value): - if self.farmos_style_grid_links: - links = [] - for mtype in quantity.material_types: - url = self.request.route_url("material_types.view", uuid=mtype.uuid) - links.append(tags.link_to(str(mtype), url)) - return ", ".join(links) - - return ", ".join([str(mtype) for mtype in quantity.material_types]) - - def configure_form(self, form): - f = form - super().configure_form(f) - quantity = form.model_instance - - # material_types - f.fields.insert_after("quantity_type", "material_types") - f.set_node("material_types", MaterialTypeRefs(self.request)) - if not self.creating: - f.set_default("material_types", quantity.material_types) - - def objectify(self, form): - quantity = super().objectify(form) - data = form.validated - - self.set_material_types(quantity, data["material_types"]) - - return quantity - - def set_material_types(self, quantity, desired): - model = self.app.model - session = self.Session() - - current = {mt.uuid.hex: mt for mt in quantity.material_types} - - for mtype in desired: - if mtype["uuid"] not in current: - mtype = session.get(model.MaterialType, mtype["uuid"]) - assert mtype - quantity.material_types.append(mtype) - - desired = [mtype["uuid"] for mtype in desired] - for uuid, mtype in current.items(): - if uuid not in desired: - quantity.material_types.remove(mtype) - def defaults(config, **kwargs): base = globals() @@ -457,11 +296,6 @@ def defaults(config, **kwargs): ) StandardQuantityView.defaults(config) - MaterialQuantityView = kwargs.get( - "MaterialQuantityView", base["MaterialQuantityView"] - ) - MaterialQuantityView.defaults(config) - def includeme(config): defaults(config) diff --git a/src/wuttafarm/web/views/quick/eggs.py b/src/wuttafarm/web/views/quick/eggs.py index fded73c..8aae46e 100644 --- a/src/wuttafarm/web/views/quick/eggs.py +++ b/src/wuttafarm/web/views/quick/eggs.py @@ -24,8 +24,6 @@ Quick Form for "Eggs" """ import json -import threading -import time import colander from deform.widget import SelectWidget @@ -333,43 +331,13 @@ class EggsQuickForm(QuickFormView): session.flush() if self.app.is_farmos_mirror(): - thread = threading.Thread( - target=self.auto_sync_to_farmos, - args=(log.uuid, quantity.uuid, new_unit.uuid if new_unit else None), - ) - thread.start() + 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 auto_sync_to_farmos(self, log_uuid, quantity_uuid, new_unit_uuid): - model = self.app.model - - with self.app.short_session(commit=True) as session: - if user := session.query(model.User).filter_by(username="farmos").first(): - session.info["continuum_user_id"] = user.uuid - - if new_unit_uuid: - new_unit = None - while not new_unit: - new_unit = session.get(model.Unit, new_unit_uuid) - if not new_unit: - time.sleep(0.1) - self.app.auto_sync_to_farmos(unit, client=self.farmos_client) - - quantity = None - while not quantity: - quantity = session.get(model.StandardQuantity, quantity_uuid) - if not quantity: - time.sleep(0.1) - self.app.auto_sync_to_farmos(quantity, client=self.farmos_client) - - log = None - while not log: - log = session.get(model.HarvestLog, log_uuid) - if not log: - time.sleep(0.1) - self.app.auto_sync_to_farmos(log, client=self.farmos_client) - def redirect_after_save(self, log): model = self.app.model diff --git a/src/wuttafarm/web/views/units.py b/src/wuttafarm/web/views/units.py index 13cff36..fe8dafe 100644 --- a/src/wuttafarm/web/views/units.py +++ b/src/wuttafarm/web/views/units.py @@ -37,19 +37,17 @@ class MeasureView(WuttaFarmMasterView): url_prefix = "/measures" grid_columns = [ - "ordinal", "name", "drupal_id", ] - sort_defaults = "ordinal" + sort_defaults = "name" filter_defaults = { "name": {"active": True, "verb": "contains"}, } form_fields = [ - "ordinal", "name", "drupal_id", ] diff --git a/src/wuttafarm/web/views/water.py b/src/wuttafarm/web/views/water.py deleted file mode 100644 index c0d551e..0000000 --- a/src/wuttafarm/web/views/water.py +++ /dev/null @@ -1,59 +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 . -# -################################################################################ -""" -Master view for Water Assets -""" - -from wuttafarm.db.model import WaterAsset -from wuttafarm.web.views.assets import AssetMasterView - - -class WaterAssetView(AssetMasterView): - """ - Master view for Plant Assets - """ - - model_class = WaterAsset - route_prefix = "water_assets" - url_prefix = "/assets/water" - - farmos_bundle = "water" - farmos_refurl_path = "/assets/water" - - grid_columns = [ - "thumbnail", - "drupal_id", - "asset_name", - "parents", - "archived", - ] - - -def defaults(config, **kwargs): - base = globals() - - WaterAssetView = kwargs.get("WaterAssetView", base["WaterAssetView"]) - WaterAssetView.defaults(config) - - -def includeme(config): - defaults(config) diff --git a/src/wuttafarm/web/views/webhooks.py b/src/wuttafarm/web/views/webhooks.py deleted file mode 100644 index f5a5db5..0000000 --- a/src/wuttafarm/web/views/webhooks.py +++ /dev/null @@ -1,96 +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 . -# -################################################################################ -""" -Views for use as webhooks -""" - -import logging -from uuid import UUID - -from wuttaweb.views import View -from wuttaweb.db import Session - - -log = logging.getLogger(__name__) - - -class WebhookView(View): - """ - Webhook views - """ - - def farmos_webhook(self): - model = self.app.model - session = Session() - - try: - data = self.request.json - log.debug("got webhook payload: %s", data) - - _, entity_type, event_type = data["event"].split(":") - - uuid = data["entity"]["uuid"][0]["value"] - if entity_type == "taxonomy_term": - bundle = data["entity"]["vid"][0]["target_id"] - else: - bundle = data["entity"]["type"][0]["target_id"] - - change = model.WebhookChange( - entity_type=entity_type, - bundle=bundle, - farmos_uuid=UUID(uuid), - deleted=event_type == "delete", - ) - session.add(change) - - except: - log.exception("failed to process webhook request") - - return {} - - @classmethod - def defaults(cls, config): - cls._webhook_defaults(config) - - @classmethod - def _webhook_defaults(cls, config): - - # farmos webhook - config.add_route("webhooks.farmos", "/farmos/webhook", request_method="POST") - config.add_view( - cls, - attr="farmos_webhook", - route_name="webhooks.farmos", - require_csrf=False, - renderer="json", - ) - - -def defaults(config, **kwargs): - base = globals() - - WebhookView = kwargs.get("WebhookView", base["WebhookView"]) - WebhookView.defaults(config) - - -def includeme(config): - defaults(config)