diff --git a/CHANGELOG.md b/CHANGELOG.md index f1eedfc..13c040e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,74 +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.8.0 (2026-03-04) - -### Feat - -- improve support for exporting quantity, log data -- show related Quantity records when viewing a Measure -- show related Quantity records when viewing a Unit -- show link to Log record when viewing Quantity - -### Fix - -- bump version requirement for wuttaweb - -## v0.7.0 (2026-03-04) - -### Feat - -- expose "group membership" for assets -- expose "current location" for assets -- add schema, sync support for `Log.is_movement` -- add schema, import support for `Asset.owners` -- add schema, import support for `Log.quick` -- show quantities when viewing log -- add sync support for `MedicalLog.vet` -- add schema, import support for `Log.quantities` -- add schema, import support for `Log.groups` -- add schema, import support for `Log.locations` -- add sync support for `Log.is_group_assignment` -- add support for exporting log status, timestamp to farmOS -- add support for log 'owners' -- add support for edit, import/export of plant type data -- add way to create animal type when editing animal -- add related version tables for asset/log revision history -- improve mirror/deletion for assets, logs, animal types -- auto-delete asset from farmOS if deleting via mirror app - -### Fix - -- show drupal ID column for asset types -- remove unique constraint for `LandAsset.land_type_uuid` -- move farmOS UUID field below the Drupal ID -- add links for Parents column in All Assets grid -- set timestamp for new log in quick eggs form -- set default grid pagesize to 50 -- add placeholder for log 'quick' field -- define log grid columns to match farmOS -- make AllLogView inherit from LogMasterView -- rename views for "all records" (all assets, all logs etc.) -- ensure token refresh works regardless where API client is used -- render links for Plant Type column in Plant Assets grid -- fix land asset type -- prevent edit for asset types, land types when app is mirror -- add farmOS-style links for Parents column in Land Assets grid -- remove unique constraint for `AnimalType.name` -- prevent delete if animal type is still being referenced -- add reminder to restart if changing integration mode -- prevent edit for user farmos_uuid, drupal_id -- remove 'contains' verb for sex filter -- add enum, row hilite for log status -- fix Sex field when empty and deleting an animal -- add `get_farmos_client_for_user()` convenience function -- use current user token for auto-sync within web app -- set log type, status enums for log grids -- add more default perms for first site admin user -- only show quick form menu if perms allow -- expose config for farmOS OAuth2 client_id and scope -- add separate permission for each quick form view - ## v0.6.0 (2026-02-25) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 1bb1dda..c66f0b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "WuttaFarm" -version = "0.8.0" +version = "0.6.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.29.0", + "WuttaWeb[continuum]>=0.28.1", ] diff --git a/src/wuttafarm/app.py b/src/wuttafarm/app.py index cb9aed3..d0ca392 100644 --- a/src/wuttafarm/app.py +++ b/src/wuttafarm/app.py @@ -36,21 +36,6 @@ class WuttaFarmAppHandler(base.AppHandler): default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler" default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler" - def get_asset_handler(self): - """ - Get the configured asset handler. - - :rtype: :class:`~wuttafarm.assets.AssetHandler` - """ - if "asset" not in self.handlers: - spec = self.config.get( - f"{self.appname}.asset_handler", - default="wuttafarm.assets:AssetHandler", - ) - factory = self.load_object(spec) - self.handlers["asset"] = factory(self.config) - return self.handlers["asset"] - def get_farmos_handler(self): """ Get the configured farmOS integration handler. @@ -151,7 +136,7 @@ class WuttaFarmAppHandler(base.AppHandler): factory = self.load_object(spec) return factory(self.config, farmos_client) - def auto_sync_to_farmos(self, obj, model_name=None, client=None, require=True): + def auto_sync_to_farmos(self, obj, model_name=None, require=True): """ Export the given object to farmOS, using configured handler. @@ -162,9 +147,6 @@ class WuttaFarmAppHandler(base.AppHandler): :param obj: Any data object in WuttaFarm, e.g. AnimalAsset instance. - :param client: Existing farmOS API client to use. If not - specified, a new one will be instantiated. - :param require: If true, this will *require* the export handler to support objects of the given type. If false, then nothing will happen / export is silently skipped when @@ -180,12 +162,14 @@ class WuttaFarmAppHandler(base.AppHandler): return # nb. begin txn to establish the API client - handler.begin_target_transaction(client) + # TODO: should probably use current user oauth2 token instead + # of always making a new one here, which is what happens IIUC + handler.begin_target_transaction() importer = handler.get_importer(model_name, caches_target=False) normal = importer.normalize_source_object(obj) importer.process_data(source_data=[normal]) - def auto_sync_from_farmos(self, obj, model_name, client=None, require=True): + def auto_sync_from_farmos(self, obj, model_name, require=True): """ Import the given object from farmOS, using configured handler. @@ -194,9 +178,6 @@ class WuttaFarmAppHandler(base.AppHandler): :param model_name': Model name for the importer to use, e.g. ``"AnimalAsset"``. - :param client: Existing farmOS API client to use. If not - specified, a new one will be instantiated. - :param require: If true, this will *require* the import handler to support objects of the given type. If false, then nothing will happen / import is silently skipped when @@ -210,7 +191,9 @@ class WuttaFarmAppHandler(base.AppHandler): return # nb. begin txn to establish the API client - handler.begin_source_transaction(client) + # TODO: should probably use current user oauth2 token instead + # of always making a new one here, which is what happens IIUC + handler.begin_source_transaction() with self.short_session(commit=True) as session: handler.target_session = session importer = handler.get_importer(model_name, caches_target=False) diff --git a/src/wuttafarm/assets.py b/src/wuttafarm/assets.py deleted file mode 100644 index 36d3b22..0000000 --- a/src/wuttafarm/assets.py +++ /dev/null @@ -1,65 +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 . -# -################################################################################ -""" -Asset handler -""" - -from wuttjamaican.app import GenericHandler - - -class AssetHandler(GenericHandler): - """ - Base class and default implementation for the asset - :term:`handler`. - """ - - def get_groups(self, asset): - model = self.app.model - session = self.app.get_session(asset) - - grplog = ( - session.query(model.Log) - .join(model.LogAsset) - .filter(model.LogAsset.asset == asset) - .filter(model.Log.is_group_assignment == True) - .order_by(model.Log.timestamp.desc()) - .first() - ) - if grplog: - return grplog.groups - return [] - - def get_locations(self, asset): - model = self.app.model - session = self.app.get_session(asset) - - loclog = ( - session.query(model.Log) - .join(model.LogAsset) - .filter(model.LogAsset.asset == asset) - .filter(model.Log.is_movement == True) - .order_by(model.Log.timestamp.desc()) - .first() - ) - if loclog: - return loclog.locations - return [] diff --git a/src/wuttafarm/config.py b/src/wuttafarm/config.py index b0c860b..16a7578 100644 --- a/src/wuttafarm/config.py +++ b/src/wuttafarm/config.py @@ -50,12 +50,11 @@ class WuttaFarmConfig(WuttaConfigExtension): f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler" ) - # web app stuff + # web app menu config.setdefault( f"{config.appname}.web.menus.handler.default_spec", "wuttafarm.web.menus:WuttaFarmMenuHandler", ) - config.setdefault("wuttaweb.grids.default_pagesize", "50") # web app libcache # config.setdefault('wuttaweb.static_libcache.module', 'wuttafarm.web.static') diff --git a/src/wuttafarm/db/alembic/versions/0771322957bd_add_log_is_movement.py b/src/wuttafarm/db/alembic/versions/0771322957bd_add_log_is_movement.py deleted file mode 100644 index 0aa9d54..0000000 --- a/src/wuttafarm/db/alembic/versions/0771322957bd_add_log_is_movement.py +++ /dev/null @@ -1,37 +0,0 @@ -"""add Log.is_movement - -Revision ID: 0771322957bd -Revises: 12de43facb95 -Create Date: 2026-03-02 20:21:03.889847 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "0771322957bd" -down_revision: Union[str, None] = "12de43facb95" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log - op.add_column("log", sa.Column("is_movement", sa.Boolean(), nullable=True)) - op.add_column( - "log_version", - sa.Column("is_movement", sa.Boolean(), autoincrement=False, nullable=True), - ) - - -def downgrade() -> None: - - # log - op.drop_column("log_version", "is_movement") - op.drop_column("log", "is_movement") diff --git a/src/wuttafarm/db/alembic/versions/12de43facb95_add_asset_owners.py b/src/wuttafarm/db/alembic/versions/12de43facb95_add_asset_owners.py deleted file mode 100644 index 67a4c25..0000000 --- a/src/wuttafarm/db/alembic/versions/12de43facb95_add_asset_owners.py +++ /dev/null @@ -1,114 +0,0 @@ -"""add Asset.owners - -Revision ID: 12de43facb95 -Revises: 85d4851e8292 -Create Date: 2026-03-02 19:03:35.511398 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "12de43facb95" -down_revision: Union[str, None] = "85d4851e8292" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # asset_owner - op.create_table( - "asset_owner", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("user_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["asset_uuid"], ["asset.uuid"], name=op.f("fk_asset_owner_asset_uuid_asset") - ), - sa.ForeignKeyConstraint( - ["user_uuid"], ["user.uuid"], name=op.f("fk_asset_owner_user_uuid_user") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_owner")), - ) - op.create_table( - "asset_owner_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "asset_uuid", - wuttjamaican.db.util.UUID(), - autoincrement=False, - nullable=True, - ), - sa.Column( - "user_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_owner_version") - ), - ) - op.create_index( - op.f("ix_asset_owner_version_end_transaction_id"), - "asset_owner_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_owner_version_operation_type"), - "asset_owner_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_asset_owner_version_pk_transaction_id", - "asset_owner_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_asset_owner_version_pk_validity", - "asset_owner_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_asset_owner_version_transaction_id"), - "asset_owner_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # asset_owner - op.drop_index( - op.f("ix_asset_owner_version_transaction_id"), table_name="asset_owner_version" - ) - op.drop_index( - "ix_asset_owner_version_pk_validity", table_name="asset_owner_version" - ) - op.drop_index( - "ix_asset_owner_version_pk_transaction_id", table_name="asset_owner_version" - ) - op.drop_index( - op.f("ix_asset_owner_version_operation_type"), table_name="asset_owner_version" - ) - op.drop_index( - op.f("ix_asset_owner_version_end_transaction_id"), - table_name="asset_owner_version", - ) - op.drop_table("asset_owner_version") - op.drop_table("asset_owner") diff --git a/src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py b/src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py deleted file mode 100644 index 0ed92d9..0000000 --- a/src/wuttafarm/db/alembic/versions/3bef7d380a38_add_loglocation.py +++ /dev/null @@ -1,118 +0,0 @@ -"""add LogLocation - -Revision ID: 3bef7d380a38 -Revises: f3c7e273bfa3 -Create Date: 2026-02-28 20:41:56.051847 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "3bef7d380a38" -down_revision: Union[str, None] = "f3c7e273bfa3" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log_location - op.create_table( - "log_location", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["asset_uuid"], - ["asset.uuid"], - name=op.f("fk_log_location_asset_uuid_asset"), - ), - sa.ForeignKeyConstraint( - ["log_uuid"], ["log.uuid"], name=op.f("fk_log_location_log_uuid_log") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_location")), - ) - op.create_table( - "log_location_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True - ), - sa.Column( - "asset_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_log_location_version") - ), - ) - op.create_index( - op.f("ix_log_location_version_end_transaction_id"), - "log_location_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_location_version_operation_type"), - "log_location_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_log_location_version_pk_transaction_id", - "log_location_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_log_location_version_pk_validity", - "log_location_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_location_version_transaction_id"), - "log_location_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # log_location - op.drop_index( - op.f("ix_log_location_version_transaction_id"), - table_name="log_location_version", - ) - op.drop_index( - "ix_log_location_version_pk_validity", table_name="log_location_version" - ) - op.drop_index( - "ix_log_location_version_pk_transaction_id", table_name="log_location_version" - ) - op.drop_index( - op.f("ix_log_location_version_operation_type"), - table_name="log_location_version", - ) - op.drop_index( - op.f("ix_log_location_version_end_transaction_id"), - table_name="log_location_version", - ) - op.drop_table("log_location_version") - op.drop_table("log_location") diff --git a/src/wuttafarm/db/alembic/versions/45c7718d2ed2_remove_unique_for_animal_type_name.py b/src/wuttafarm/db/alembic/versions/45c7718d2ed2_remove_unique_for_animal_type_name.py deleted file mode 100644 index 03759cf..0000000 --- a/src/wuttafarm/db/alembic/versions/45c7718d2ed2_remove_unique_for_animal_type_name.py +++ /dev/null @@ -1,37 +0,0 @@ -"""remove unique for animal_type.name - -Revision ID: 45c7718d2ed2 -Revises: 5b6c87d8cddf -Create Date: 2026-02-27 16:53:59.310342 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "45c7718d2ed2" -down_revision: Union[str, None] = "5b6c87d8cddf" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # animal_type - op.drop_constraint(op.f("uq_animal_type_name"), "animal_type", type_="unique") - - -def downgrade() -> None: - - # animal_type - op.create_unique_constraint( - op.f("uq_animal_type_name"), - "animal_type", - ["name"], - postgresql_nulls_not_distinct=False, - ) diff --git a/src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py b/src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py deleted file mode 100644 index 8dffce9..0000000 --- a/src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py +++ /dev/null @@ -1,108 +0,0 @@ -"""add LogOwner - -Revision ID: 47d0ebd84554 -Revises: 45c7718d2ed2 -Create Date: 2026-02-28 19:18:49.122090 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "47d0ebd84554" -down_revision: Union[str, None] = "45c7718d2ed2" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log_owner - op.create_table( - "log_owner", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("user_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["log_uuid"], ["log.uuid"], name=op.f("fk_log_owner_log_uuid_log") - ), - sa.ForeignKeyConstraint( - ["user_uuid"], ["user.uuid"], name=op.f("fk_log_owner_user_uuid_user") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_owner")), - ) - op.create_table( - "log_owner_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True - ), - sa.Column( - "user_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_log_owner_version") - ), - ) - op.create_index( - op.f("ix_log_owner_version_end_transaction_id"), - "log_owner_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_owner_version_operation_type"), - "log_owner_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_log_owner_version_pk_transaction_id", - "log_owner_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_log_owner_version_pk_validity", - "log_owner_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_owner_version_transaction_id"), - "log_owner_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # log_owner - op.drop_index( - op.f("ix_log_owner_version_transaction_id"), table_name="log_owner_version" - ) - op.drop_index("ix_log_owner_version_pk_validity", table_name="log_owner_version") - op.drop_index( - "ix_log_owner_version_pk_transaction_id", table_name="log_owner_version" - ) - op.drop_index( - op.f("ix_log_owner_version_operation_type"), table_name="log_owner_version" - ) - op.drop_index( - op.f("ix_log_owner_version_end_transaction_id"), table_name="log_owner_version" - ) - op.drop_table("log_owner_version") - op.drop_table("log_owner") diff --git a/src/wuttafarm/db/alembic/versions/5f474125a80e_remove_unwanted_unique_constraint.py b/src/wuttafarm/db/alembic/versions/5f474125a80e_remove_unwanted_unique_constraint.py deleted file mode 100644 index e5d28ab..0000000 --- a/src/wuttafarm/db/alembic/versions/5f474125a80e_remove_unwanted_unique_constraint.py +++ /dev/null @@ -1,39 +0,0 @@ -"""remove unwanted unique constraint - -Revision ID: 5f474125a80e -Revises: 0771322957bd -Create Date: 2026-03-04 12:03:16.034291 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "5f474125a80e" -down_revision: Union[str, None] = "0771322957bd" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # asset_land - op.drop_constraint( - op.f("uq_asset_land_land_type_uuid"), "asset_land", type_="unique" - ) - - -def downgrade() -> None: - - # asset_land - op.create_unique_constraint( - op.f("uq_asset_land_land_type_uuid"), - "asset_land", - ["land_type_uuid"], - postgresql_nulls_not_distinct=False, - ) diff --git a/src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py b/src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py deleted file mode 100644 index 170e3d2..0000000 --- a/src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py +++ /dev/null @@ -1,111 +0,0 @@ -"""add LogGroup - -Revision ID: 74d32b4ec210 -Revises: 3bef7d380a38 -Create Date: 2026-02-28 21:35:24.125784 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "74d32b4ec210" -down_revision: Union[str, None] = "3bef7d380a38" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log_group - op.create_table( - "log_group", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["asset_uuid"], ["asset.uuid"], name=op.f("fk_log_group_asset_uuid_asset") - ), - sa.ForeignKeyConstraint( - ["log_uuid"], ["log.uuid"], name=op.f("fk_log_group_log_uuid_log") - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_group")), - ) - op.create_table( - "log_group_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True - ), - sa.Column( - "asset_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_log_group_version") - ), - ) - op.create_index( - op.f("ix_log_group_version_end_transaction_id"), - "log_group_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_group_version_operation_type"), - "log_group_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_log_group_version_pk_transaction_id", - "log_group_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_log_group_version_pk_validity", - "log_group_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_group_version_transaction_id"), - "log_group_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # log_group - op.drop_index( - op.f("ix_log_group_version_transaction_id"), table_name="log_group_version" - ) - op.drop_index("ix_log_group_version_pk_validity", table_name="log_group_version") - op.drop_index( - "ix_log_group_version_pk_transaction_id", table_name="log_group_version" - ) - op.drop_index( - op.f("ix_log_group_version_operation_type"), table_name="log_group_version" - ) - op.drop_index( - op.f("ix_log_group_version_end_transaction_id"), table_name="log_group_version" - ) - op.drop_table("log_group_version") - op.drop_table("log_group") diff --git a/src/wuttafarm/db/alembic/versions/85d4851e8292_add_log_quick.py b/src/wuttafarm/db/alembic/versions/85d4851e8292_add_log_quick.py deleted file mode 100644 index 97e87bc..0000000 --- a/src/wuttafarm/db/alembic/versions/85d4851e8292_add_log_quick.py +++ /dev/null @@ -1,37 +0,0 @@ -"""add Log.quick - -Revision ID: 85d4851e8292 -Revises: d459db991404 -Create Date: 2026-03-02 18:42:56.070281 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "85d4851e8292" -down_revision: Union[str, None] = "d459db991404" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log - op.add_column("log", sa.Column("quick", sa.String(length=20), nullable=True)) - op.add_column( - "log_version", - sa.Column("quick", sa.String(length=20), autoincrement=False, nullable=True), - ) - - -def downgrade() -> None: - - # log - op.drop_column("log_version", "quick") - op.drop_column("log", "quick") diff --git a/src/wuttafarm/db/alembic/versions/9e875e5cbdc1_add_logquantity.py b/src/wuttafarm/db/alembic/versions/9e875e5cbdc1_add_logquantity.py deleted file mode 100644 index 3867b17..0000000 --- a/src/wuttafarm/db/alembic/versions/9e875e5cbdc1_add_logquantity.py +++ /dev/null @@ -1,118 +0,0 @@ -"""add LogQuantity - -Revision ID: 9e875e5cbdc1 -Revises: 74d32b4ec210 -Create Date: 2026-02-28 21:55:31.876087 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "9e875e5cbdc1" -down_revision: Union[str, None] = "74d32b4ec210" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log_quantity - op.create_table( - "log_quantity", - sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.Column("quantity_uuid", wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint( - ["log_uuid"], ["log.uuid"], name=op.f("fk_log_quantity_log_uuid_log") - ), - sa.ForeignKeyConstraint( - ["quantity_uuid"], - ["quantity.uuid"], - name=op.f("fk_log_quantity_quantity_uuid_quantity"), - ), - sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_quantity")), - ) - op.create_table( - "log_quantity_version", - sa.Column( - "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False - ), - sa.Column( - "log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True - ), - sa.Column( - "quantity_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_log_quantity_version") - ), - ) - op.create_index( - op.f("ix_log_quantity_version_end_transaction_id"), - "log_quantity_version", - ["end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_quantity_version_operation_type"), - "log_quantity_version", - ["operation_type"], - unique=False, - ) - op.create_index( - "ix_log_quantity_version_pk_transaction_id", - "log_quantity_version", - ["uuid", sa.literal_column("transaction_id DESC")], - unique=False, - ) - op.create_index( - "ix_log_quantity_version_pk_validity", - "log_quantity_version", - ["uuid", "transaction_id", "end_transaction_id"], - unique=False, - ) - op.create_index( - op.f("ix_log_quantity_version_transaction_id"), - "log_quantity_version", - ["transaction_id"], - unique=False, - ) - - -def downgrade() -> None: - - # log_quantity - op.drop_index( - op.f("ix_log_quantity_version_transaction_id"), - table_name="log_quantity_version", - ) - op.drop_index( - "ix_log_quantity_version_pk_validity", table_name="log_quantity_version" - ) - op.drop_index( - "ix_log_quantity_version_pk_transaction_id", table_name="log_quantity_version" - ) - op.drop_index( - op.f("ix_log_quantity_version_operation_type"), - table_name="log_quantity_version", - ) - op.drop_index( - op.f("ix_log_quantity_version_end_transaction_id"), - table_name="log_quantity_version", - ) - op.drop_table("log_quantity_version") - op.drop_table("log_quantity") diff --git a/src/wuttafarm/db/alembic/versions/d459db991404_add_medicallog_vet.py b/src/wuttafarm/db/alembic/versions/d459db991404_add_medicallog_vet.py deleted file mode 100644 index c65c93e..0000000 --- a/src/wuttafarm/db/alembic/versions/d459db991404_add_medicallog_vet.py +++ /dev/null @@ -1,37 +0,0 @@ -"""add MedicalLog.vet - -Revision ID: d459db991404 -Revises: 9e875e5cbdc1 -Create Date: 2026-02-28 22:17:57.001134 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "d459db991404" -down_revision: Union[str, None] = "9e875e5cbdc1" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log_medical - op.add_column("log_medical", sa.Column("vet", sa.String(length=100), nullable=True)) - op.add_column( - "log_medical_version", - sa.Column("vet", sa.String(length=100), autoincrement=False, nullable=True), - ) - - -def downgrade() -> None: - - # log_medical - op.drop_column("log_medical_version", "vet") - op.drop_column("log_medical", "vet") diff --git a/src/wuttafarm/db/alembic/versions/f3c7e273bfa3_add_log_is_group_assignment.py b/src/wuttafarm/db/alembic/versions/f3c7e273bfa3_add_log_is_group_assignment.py deleted file mode 100644 index 986f4db..0000000 --- a/src/wuttafarm/db/alembic/versions/f3c7e273bfa3_add_log_is_group_assignment.py +++ /dev/null @@ -1,39 +0,0 @@ -"""add Log.is_group_assignment - -Revision ID: f3c7e273bfa3 -Revises: 47d0ebd84554 -Create Date: 2026-02-28 20:04:40.700474 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util - - -# revision identifiers, used by Alembic. -revision: str = "f3c7e273bfa3" -down_revision: Union[str, None] = "47d0ebd84554" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # log - op.add_column("log", sa.Column("is_group_assignment", sa.Boolean(), nullable=True)) - op.add_column( - "log_version", - sa.Column( - "is_group_assignment", sa.Boolean(), autoincrement=False, nullable=True - ), - ) - - -def downgrade() -> None: - - # log - op.drop_column("log_version", "is_group_assignment") - op.drop_column("log", "is_group_assignment") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index 15514fb..68695e5 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -38,7 +38,7 @@ from .asset_structure import StructureType, StructureAsset from .asset_animal import AnimalType, AnimalAsset from .asset_group import GroupAsset from .asset_plant import PlantType, PlantAsset, PlantAssetPlantType -from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner +from .log import LogType, Log, LogAsset from .log_activity import ActivityLog from .log_harvest import HarvestLog from .log_medical import MedicalLog diff --git a/src/wuttafarm/db/model/asset.py b/src/wuttafarm/db/model/asset.py index 0face47..90372e2 100644 --- a/src/wuttafarm/db/model/asset.py +++ b/src/wuttafarm/db/model/asset.py @@ -26,7 +26,6 @@ Model definition for Asset Types import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db import model @@ -187,25 +186,6 @@ class Asset(model.Base): cascade_backrefs=False, ) - parents = association_proxy( - "_parents", - "parent", - creator=lambda parent: AssetParent(parent=parent), - ) - - _owners = orm.relationship( - "AssetOwner", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="asset", - ) - - owners = association_proxy( - "_owners", - "user", - creator=lambda user: AssetOwner(user=user), - ) - def __str__(self): return self.asset_name or "" @@ -216,12 +196,7 @@ class AssetMixin: @declared_attr def asset(cls): - return orm.relationship( - Asset, - single_parent=True, - cascade="all, delete-orphan", - cascade_backrefs=False, - ) + return orm.relationship(Asset) def __str__(self): return self.asset_name or "" @@ -238,8 +213,6 @@ def add_asset_proxies(subclass): Asset.make_proxy(subclass, "asset", "thumbnail_url") Asset.make_proxy(subclass, "asset", "image_url") Asset.make_proxy(subclass, "asset", "archived") - Asset.make_proxy(subclass, "asset", "parents") - Asset.make_proxy(subclass, "asset", "owners") class EggMixin: @@ -277,27 +250,3 @@ class AssetParent(model.Base): Asset, foreign_keys=parent_uuid, ) - - -class AssetOwner(model.Base): - """ - Represents a "asset's owner relationship" from farmOS. - """ - - __tablename__ = "asset_owner" - __versioned__ = {} - - uuid = model.uuid_column() - - asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False) - asset = orm.relationship( - Asset, - foreign_keys=asset_uuid, - back_populates="_owners", - ) - - user_uuid = model.uuid_fk_column("user.uuid", nullable=False) - user = orm.relationship( - model.User, - foreign_keys=user_uuid, - ) diff --git a/src/wuttafarm/db/model/asset_animal.py b/src/wuttafarm/db/model/asset_animal.py index 443a984..768b0f9 100644 --- a/src/wuttafarm/db/model/asset_animal.py +++ b/src/wuttafarm/db/model/asset_animal.py @@ -48,6 +48,7 @@ class AnimalType(model.Base): name = sa.Column( sa.String(length=100), nullable=False, + unique=True, doc=""" Name of the animal type. """, @@ -79,14 +80,6 @@ class AnimalType(model.Base): """, ) - animal_assets = orm.relationship( - "AnimalAsset", - doc=""" - List of animal assets of this type. - """, - back_populates="animal_type", - ) - def __str__(self): return self.name or "" @@ -110,7 +103,6 @@ class AnimalAsset(AssetMixin, EggMixin, model.Base): doc=""" Reference to the animal type. """, - back_populates="animal_assets", ) birthdate = sa.Column( diff --git a/src/wuttafarm/db/model/asset_land.py b/src/wuttafarm/db/model/asset_land.py index 6c65c54..bbd7bf0 100644 --- a/src/wuttafarm/db/model/asset_land.py +++ b/src/wuttafarm/db/model/asset_land.py @@ -88,10 +88,10 @@ class LandAsset(AssetMixin, model.Base): __wutta_hint__ = { "model_title": "Land Asset", "model_title_plural": "Land Assets", - "farmos_asset_type": "land", + "farmos_asset_type": "animal", } - land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False) + land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False, unique=True) land_type = orm.relationship(LandType, back_populates="land_assets") diff --git a/src/wuttafarm/db/model/asset_plant.py b/src/wuttafarm/db/model/asset_plant.py index 62f7e9b..5f10e7c 100644 --- a/src/wuttafarm/db/model/asset_plant.py +++ b/src/wuttafarm/db/model/asset_plant.py @@ -25,7 +25,6 @@ Model definition for Plant Assets import sqlalchemy as sa from sqlalchemy import orm -from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db import model @@ -81,12 +80,6 @@ class PlantType(model.Base): """, ) - _plant_assets = orm.relationship( - "PlantAssetPlantType", - cascade_backrefs=False, - back_populates="plant_type", - ) - def __str__(self): return self.name or "" @@ -106,17 +99,9 @@ class PlantAsset(AssetMixin, model.Base): _plant_types = orm.relationship( "PlantAssetPlantType", - cascade="all, delete-orphan", - cascade_backrefs=False, back_populates="plant_asset", ) - plant_types = association_proxy( - "_plant_types", - "plant_type", - creator=lambda pt: PlantAssetPlantType(plant_type=pt), - ) - add_asset_proxies(PlantAsset) @@ -144,5 +129,4 @@ class PlantAssetPlantType(model.Base): doc=""" Reference to the plant type. """, - back_populates="_plant_assets", ) diff --git a/src/wuttafarm/db/model/log.py b/src/wuttafarm/db/model/log.py index 7823353..a86c447 100644 --- a/src/wuttafarm/db/model/log.py +++ b/src/wuttafarm/db/model/log.py @@ -26,7 +26,6 @@ Model definition for Logs import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db import model @@ -120,22 +119,6 @@ class Log(model.Base): """, ) - is_movement = sa.Column( - sa.Boolean(), - nullable=True, - doc=""" - Whether the log represents a movement to new location. - """, - ) - - is_group_assignment = sa.Column( - sa.Boolean(), - nullable=True, - doc=""" - Whether the log represents a group assignment. - """, - ) - status = sa.Column( sa.String(length=20), nullable=False, @@ -152,15 +135,6 @@ class Log(model.Base): """, ) - quick = sa.Column( - sa.String(length=20), - nullable=True, - doc=""" - Identifier of quick form used to create the log, if - applicable. - """, - ) - farmos_uuid = sa.Column( model.UUID(), nullable=True, @@ -179,70 +153,7 @@ class Log(model.Base): """, ) - _assets = orm.relationship( - "LogAsset", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="log", - ) - - assets = association_proxy( - "_assets", - "asset", - creator=lambda asset: LogAsset(asset=asset), - ) - - _groups = orm.relationship( - "LogGroup", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="log", - ) - - groups = association_proxy( - "_groups", - "asset", - creator=lambda asset: LogGroup(asset=asset), - ) - - _locations = orm.relationship( - "LogLocation", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="log", - ) - - locations = association_proxy( - "_locations", - "asset", - creator=lambda asset: LogLocation(asset=asset), - ) - - _quantities = orm.relationship( - "LogQuantity", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="log", - ) - - quantities = association_proxy( - "_quantities", - "quantity", - creator=lambda quantity: LogQuantity(quantity=quantity), - ) - - _owners = orm.relationship( - "LogOwner", - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="log", - ) - - owners = association_proxy( - "_owners", - "user", - creator=lambda user: LogOwner(user=user), - ) + _assets = orm.relationship("LogAsset", back_populates="log") def __str__(self): return self.message or "" @@ -254,12 +165,7 @@ class LogMixin: @declared_attr def log(cls): - return orm.relationship( - Log, - single_parent=True, - cascade="all, delete-orphan", - cascade_backrefs=False, - ) + return orm.relationship(Log) def __str__(self): return self.message or "" @@ -271,16 +177,8 @@ def add_log_proxies(subclass): Log.make_proxy(subclass, "log", "log_type") Log.make_proxy(subclass, "log", "message") Log.make_proxy(subclass, "log", "timestamp") - Log.make_proxy(subclass, "log", "is_movement") - Log.make_proxy(subclass, "log", "is_group_assignment") Log.make_proxy(subclass, "log", "status") Log.make_proxy(subclass, "log", "notes") - Log.make_proxy(subclass, "log", "quick") - Log.make_proxy(subclass, "log", "assets") - Log.make_proxy(subclass, "log", "groups") - Log.make_proxy(subclass, "log", "locations") - Log.make_proxy(subclass, "log", "quantities") - Log.make_proxy(subclass, "log", "owners") class LogAsset(model.Base): @@ -305,100 +203,3 @@ class LogAsset(model.Base): "Asset", foreign_keys=asset_uuid, ) - - -class LogGroup(model.Base): - """ - Represents a "log's group relationship" from farmOS. - """ - - __tablename__ = "log_group" - __versioned__ = {} - - uuid = model.uuid_column() - - log_uuid = model.uuid_fk_column("log.uuid", nullable=False) - log = orm.relationship( - Log, - foreign_keys=log_uuid, - back_populates="_groups", - ) - - asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False) - asset = orm.relationship( - "Asset", - foreign_keys=asset_uuid, - ) - - -class LogLocation(model.Base): - """ - Represents a "log's location relationship" from farmOS. - """ - - __tablename__ = "log_location" - __versioned__ = {} - - uuid = model.uuid_column() - - log_uuid = model.uuid_fk_column("log.uuid", nullable=False) - log = orm.relationship( - Log, - foreign_keys=log_uuid, - back_populates="_locations", - ) - - asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False) - asset = orm.relationship( - "Asset", - foreign_keys=asset_uuid, - ) - - -class LogQuantity(model.Base): - """ - Represents a "log's quantity relationship" from farmOS. - """ - - __tablename__ = "log_quantity" - __versioned__ = {} - - uuid = model.uuid_column() - - log_uuid = model.uuid_fk_column("log.uuid", nullable=False) - log = orm.relationship( - Log, - foreign_keys=log_uuid, - back_populates="_quantities", - ) - - quantity_uuid = model.uuid_fk_column("quantity.uuid", nullable=False) - quantity = orm.relationship( - "Quantity", - foreign_keys=quantity_uuid, - back_populates="_log", - ) - - -class LogOwner(model.Base): - """ - Represents a "log's owner relationship" from farmOS. - """ - - __tablename__ = "log_owner" - __versioned__ = {} - - uuid = model.uuid_column() - - log_uuid = model.uuid_fk_column("log.uuid", nullable=False) - log = orm.relationship( - Log, - foreign_keys=log_uuid, - back_populates="_owners", - ) - - user_uuid = model.uuid_fk_column("user.uuid", nullable=False) - user = orm.relationship( - model.User, - foreign_keys=user_uuid, - ) diff --git a/src/wuttafarm/db/model/log_medical.py b/src/wuttafarm/db/model/log_medical.py index 6cf308f..439ee3b 100644 --- a/src/wuttafarm/db/model/log_medical.py +++ b/src/wuttafarm/db/model/log_medical.py @@ -23,8 +23,6 @@ Model definition for Medical Logs """ -import sqlalchemy as sa - from wuttjamaican.db import model from wuttafarm.db.model.log import LogMixin, add_log_proxies @@ -43,13 +41,5 @@ class MedicalLog(LogMixin, model.Base): "farmos_log_type": "medical", } - vet = sa.Column( - sa.String(length=100), - nullable=True, - doc=""" - Name of the veterinarian, if applicable. - """, - ) - add_log_proxies(MedicalLog) diff --git a/src/wuttafarm/db/model/quantities.py b/src/wuttafarm/db/model/quantities.py index 4bed6a0..4f537b9 100644 --- a/src/wuttafarm/db/model/quantities.py +++ b/src/wuttafarm/db/model/quantities.py @@ -26,7 +26,6 @@ Model definition for Quantities import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db import model @@ -162,25 +161,6 @@ class Quantity(model.Base): """, ) - _log = orm.relationship( - "LogQuantity", - uselist=False, - cascade="all, delete-orphan", - cascade_backrefs=False, - back_populates="quantity", - ) - - def make_log_quantity(log): - from wuttafarm.db.model import LogQuantity - - return LogQuantity(log=log) - - log = association_proxy( - "_log", - "log", - creator=make_log_quantity, - ) - def render_as_text(self, config=None): measure = str(self.measure or self.measure_id or "") value = self.value_numerator / self.value_denominator @@ -222,7 +202,6 @@ def add_quantity_proxies(subclass): Quantity.make_proxy(subclass, "quantity", "units_uuid") Quantity.make_proxy(subclass, "quantity", "units") Quantity.make_proxy(subclass, "quantity", "label") - Quantity.make_proxy(subclass, "quantity", "log") class StandardQuantity(QuantityMixin, model.Base): diff --git a/src/wuttafarm/farmos/handler.py b/src/wuttafarm/farmos/handler.py index e905f92..6eee14f 100644 --- a/src/wuttafarm/farmos/handler.py +++ b/src/wuttafarm/farmos/handler.py @@ -94,9 +94,3 @@ class FarmOSHandler(GenericHandler): return f"{base}/{path}" return base - - def get_oauth2_client_id(self): - return self.config.get("farmos.oauth2.client_id", default="farm") - - def get_oauth2_scope(self): - return self.config.get("farmos.oauth2.scope", default="farm_manager") diff --git a/src/wuttafarm/farmos/importing/model.py b/src/wuttafarm/farmos/importing/model.py index ad1cb38..337649c 100644 --- a/src/wuttafarm/farmos/importing/model.py +++ b/src/wuttafarm/farmos/importing/model.py @@ -347,12 +347,6 @@ class LandAssetImporter(ToFarmOSAsset): return payload -class PlantTypeImporter(ToFarmOSTaxonomy): - - model_title = "PlantType" - farmos_taxonomy_type = "plant_type" - - class PlantAssetImporter(ToFarmOSAsset): model_title = "PlantAsset" @@ -443,138 +437,6 @@ class StructureAssetImporter(ToFarmOSAsset): return payload -############################## -# quantity importers -############################## - - -class ToFarmOSQuantity(ToFarmOS): - """ - Base class for quantity data importer targeting the farmOS API. - """ - - farmos_quantity_type = None - - supported_fields = [ - "uuid", - "measure", - "value_numerator", - "value_denominator", - "label", - "quantity_type_uuid", - "unit_uuid", - ] - - def get_target_objects(self, **kwargs): - return list( - self.farmos_client.resource.iterate("quantity", self.farmos_quantity_type) - ) - - def get_target_object(self, key): - - # fetch from cache, if applicable - if self.caches_target: - return super().get_target_object(key) - - # okay now must fetch via API - if self.get_keys() != ["uuid"]: - raise ValueError("must use uuid key for this to work") - uuid = key[0] - - try: - qty = self.farmos_client.resource.get_id( - "quantity", self.farmos_quantity_type, str(uuid) - ) - except requests.HTTPError as exc: - if exc.response.status_code == 404: - return None - raise - return qty["data"] - - def create_target_object(self, key, source_data): - if source_data.get("__ignoreme__"): - return None - if self.dry_run: - return source_data - - payload = self.get_quantity_payload(source_data) - result = self.farmos_client.resource.send( - "quantity", self.farmos_quantity_type, payload - ) - normal = self.normalize_target_object(result["data"]) - normal["_new_object"] = result["data"] - return normal - - def update_target_object(self, quantity, source_data, target_data=None): - if self.dry_run: - return quantity - - payload = self.get_quantity_payload(source_data) - payload["id"] = str(source_data["uuid"]) - result = self.farmos_client.resource.send( - "quantity", self.farmos_quantity_type, payload - ) - return self.normalize_target_object(result["data"]) - - def normalize_target_object(self, qty): - - result = { - "uuid": UUID(qty["id"]), - "measure": qty["attributes"]["measure"], - "value_numerator": qty["attributes"]["value"]["numerator"], - "value_denominator": qty["attributes"]["value"]["denominator"], - "label": qty["attributes"]["label"], - "quantity_type_uuid": UUID( - qty["relationships"]["quantity_type"]["data"]["id"] - ), - "unit_uuid": None, - } - - if unit := qty["relationships"]["units"]["data"]: - result["unit_uuid"] = UUID(unit["id"]) - - return result - - def get_quantity_payload(self, source_data): - - attrs = {} - if "measure" in self.fields: - attrs["measure"] = source_data["measure"] - if "value_numerator" in self.fields and "value_denominator" in self.fields: - attrs["value"] = { - "numerator": source_data["value_numerator"], - "denominator": source_data["value_denominator"], - } - if "label" in self.fields: - attrs["label"] = source_data["label"] - - rels = {} - if "quantity_type_uuid" in self.fields: - rels["quantity_type"] = { - "data": { - "id": str(source_data["quantity_type_uuid"]), - "type": "quantity_type--quantity_type", - } - } - if "unit_uuid" in self.fields: - rels["units"] = { - "data": { - "id": str(source_data["unit_uuid"]), - "type": "taxonomy_term--unit", - } - } - - payload = {"attributes": attrs, "relationships": rels} - - return payload - - -class StandardQuantityImporter(ToFarmOSQuantity): - - model_title = "StandardQuantity" - farmos_quantity_type = "standard" - - ############################## # log importers ############################## @@ -590,20 +452,9 @@ class ToFarmOSLog(ToFarmOS): supported_fields = [ "uuid", "name", - "timestamp", - "is_movement", - "is_group_assignment", - "status", "notes", - "quick", - "assets", - "quantities", ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.normal = self.app.get_normalizer(self.farmos_client) - def get_target_objects(self, **kwargs): result = self.farmos_client.log.get(self.farmos_log_type) return result["data"] @@ -649,18 +500,14 @@ class ToFarmOSLog(ToFarmOS): return self.normalize_target_object(result["data"]) def normalize_target_object(self, log): - normal = self.normal.normalize_farmos_log(log) + + if notes := log["attributes"]["notes"]: + notes = notes["value"] + return { - "uuid": UUID(normal["uuid"]), - "name": normal["name"], - "timestamp": self.app.make_utc(normal["timestamp"]), - "is_movement": normal["is_movement"], - "is_group_assignment": normal["is_group_assignment"], - "status": normal["status"], - "notes": normal["notes"], - "quick": normal["quick"], - "assets": [(a["asset_type"], UUID(a["uuid"])) for a in normal["assets"]], - "quantities": [UUID(uuid) for uuid in normal["quantity_uuids"]], + "uuid": UUID(log["id"]), + "name": log["attributes"]["name"], + "notes": notes, } def get_log_payload(self, source_data): @@ -668,43 +515,11 @@ class ToFarmOSLog(ToFarmOS): attrs = {} if "name" in self.fields: attrs["name"] = source_data["name"] - if "timestamp" in self.fields: - attrs["timestamp"] = self.format_datetime(source_data["timestamp"]) - if "is_movement" in self.fields: - attrs["is_movement"] = source_data["is_movement"] - if "is_group_assignment" in self.fields: - attrs["is_group_assignment"] = source_data["is_group_assignment"] - if "status" in self.fields: - attrs["status"] = source_data["status"] if "notes" in self.fields: attrs["notes"] = {"value": source_data["notes"]} - if "quick" in self.fields: - attrs["quick"] = source_data["quick"] - rels = {} - if "assets" in self.fields: - assets = [] - for asset_type, uuid in source_data["assets"]: - assets.append( - { - "type": f"asset--{asset_type}", - "id": str(uuid), - } - ) - rels["asset"] = {"data": assets} - if "quantities" in self.fields: - quantities = [] - for uuid in source_data["quantities"]: - quantities.append( - { - # TODO: support other quantity types - "type": "quantity--standard", - "id": str(uuid), - } - ) - rels["quantity"] = {"data": quantities} + payload = {"attributes": attrs} - payload = {"attributes": attrs, "relationships": rels} return payload @@ -725,32 +540,6 @@ class MedicalLogImporter(ToFarmOSLog): model_title = "MedicalLog" farmos_log_type = "medical" - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "vet", - ] - ) - return fields - - def normalize_target_object(self, log): - data = super().normalize_target_object(log) - data.update( - { - "vet": log["attributes"]["vet"], - } - ) - return data - - def get_log_payload(self, source_data): - payload = super().get_log_payload(source_data) - - if "vet" in self.fields: - payload["attributes"]["vet"] = source_data["vet"] - - return payload - class ObservationLogImporter(ToFarmOSLog): diff --git a/src/wuttafarm/farmos/importing/wuttafarm.py b/src/wuttafarm/farmos/importing/wuttafarm.py index 8394e4c..e11663f 100644 --- a/src/wuttafarm/farmos/importing/wuttafarm.py +++ b/src/wuttafarm/farmos/importing/wuttafarm.py @@ -50,15 +50,12 @@ class ToFarmOSHandler(ImportHandler): # TODO: a lot of duplication to cleanup here; see FromFarmOSHandler - def begin_target_transaction(self, client=None): + def begin_target_transaction(self): """ Establish the farmOS API client. """ - if client: - self.farmos_client = client - else: - token = self.get_farmos_oauth2_token() - self.farmos_client = self.app.get_farmos_client(token=token) + token = self.get_farmos_oauth2_token() + self.farmos_client = self.app.get_farmos_client(token=token) self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) def get_farmos_oauth2_token(self): @@ -101,10 +98,8 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler): importers["AnimalType"] = AnimalTypeImporter importers["AnimalAsset"] = AnimalAssetImporter importers["GroupAsset"] = GroupAssetImporter - importers["PlantType"] = PlantTypeImporter importers["PlantAsset"] = PlantAssetImporter importers["Unit"] = UnitImporter - importers["StandardQuantity"] = StandardQuantityImporter importers["ActivityLog"] = ActivityLogImporter importers["HarvestLog"] = HarvestLogImporter importers["MedicalLog"] = MedicalLogImporter @@ -268,28 +263,6 @@ class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter) } -class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter): - """ - WuttaFarm → farmOS API exporter for Plant Types - """ - - source_model_class = model.PlantType - - supported_fields = [ - "uuid", - "name", - ] - - drupal_internal_id_field = "drupal_internal__tid" - - def normalize_source_object(self, plant_type): - return { - "uuid": plant_type.farmos_uuid or self.app.make_true_uuid(), - "name": plant_type.name, - "_src_object": plant_type, - } - - class PlantAssetImporter(FromWuttaFarm, farmos_importing.model.PlantAssetImporter): """ WuttaFarm → farmOS API exporter for Plant Assets @@ -348,49 +321,6 @@ class StructureAssetImporter( } -############################## -# quantity importers -############################## - - -class FromWuttaFarmQuantity(FromWuttaFarm): - """ - Base class for WuttaFarm -> farmOS quantity importers - """ - - supported_fields = [ - "uuid", - "measure", - "value_numerator", - "value_denominator", - "label", - "quantity_type_uuid", - "unit_uuid", - ] - - def normalize_source_object(self, qty): - return { - "uuid": qty.farmos_uuid or self.app.make_true_uuid(), - "measure": qty.measure_id, - "value_numerator": qty.value_numerator, - "value_denominator": qty.value_denominator, - "label": qty.label, - "quantity_type_uuid": qty.quantity_type.farmos_uuid, - "unit_uuid": qty.units.farmos_uuid, - "_src_object": qty, - } - - -class StandardQuantityImporter( - FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter -): - """ - WuttaFarm → farmOS API exporter for Standard Quantities - """ - - source_model_class = model.StandardQuantity - - ############################## # log importers ############################## @@ -404,28 +334,14 @@ class FromWuttaFarmLog(FromWuttaFarm): supported_fields = [ "uuid", "name", - "timestamp", - "is_movement", - "is_group_assignment", - "status", "notes", - "quick", - "assets", - "quantities", ] def normalize_source_object(self, log): return { "uuid": log.farmos_uuid or self.app.make_true_uuid(), "name": log.message, - "timestamp": log.timestamp, - "is_movement": log.is_movement, - "is_group_assignment": log.is_group_assignment, - "status": log.status, "notes": log.notes, - "quick": self.config.parse_list(log.quick) if log.quick else [], - "assets": [(a.asset_type, a.farmos_uuid) for a in log.assets], - "quantities": [qty.farmos_uuid for qty in log.quantities], "_src_object": log, } @@ -453,24 +369,6 @@ class MedicalLogImporter(FromWuttaFarmLog, farmos_importing.model.MedicalLogImpo source_model_class = model.MedicalLog - def get_supported_fields(self): - fields = list(super().get_supported_fields()) - fields.extend( - [ - "vet", - ] - ) - return fields - - def normalize_source_object(self, log): - data = super().normalize_source_object(log) - data.update( - { - "vet": log.vet, - } - ) - return data - class ObservationLogImporter( FromWuttaFarmLog, farmos_importing.model.ObservationLogImporter diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index 6b21090..e17825b 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -46,15 +46,12 @@ class FromFarmOSHandler(ImportHandler): source_key = "farmos" generic_source_title = "farmOS" - def begin_source_transaction(self, client=None): + def begin_source_transaction(self): """ Establish the farmOS API client. """ - if client: - self.farmos_client = client - else: - token = self.get_farmos_oauth2_token() - self.farmos_client = self.app.get_farmos_client(token=token) + token = self.get_farmos_oauth2_token() + self.farmos_client = self.app.get_farmos_client(token=token) self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.normal = self.app.get_normalizer(self.farmos_client) @@ -187,7 +184,6 @@ class AssetImporterBase(FromFarmOS, ToWutta): fields.extend( [ "parents", - "owners", ] ) return fields @@ -195,9 +191,8 @@ class AssetImporterBase(FromFarmOS, ToWutta): def get_source_objects(self): """ """ asset_type = self.get_farmos_asset_type() - return list( - self.farmos_client.asset.iterate(asset_type, params={"include": "image"}) - ) + result = self.farmos_client.asset.get(asset_type) + return result["data"] def normalize_source_data(self, **kwargs): """ """ @@ -210,40 +205,49 @@ class AssetImporterBase(FromFarmOS, ToWutta): return data - def normalize_source_object(self, asset): + def normalize_asset(self, asset): """ """ - data = self.normal.normalize_farmos_asset(asset) + image_url = None + thumbnail_url = None + if relationships := asset.get("relationships"): - data["farmos_uuid"] = UUID(data.pop("uuid")) - data["asset_type"] = self.get_asset_type(asset) + if image := relationships.get("image"): + if image["data"]: + image = self.farmos_client.resource.get_id( + "file", "file", image["data"][0]["id"] + ) + if image_style := image["data"]["attributes"].get( + "image_style_uri" + ): + image_url = image_style["large"] + thumbnail_url = image_style["thumbnail"] - if "image_url" in self.fields or "thumbnail_url" in self.fields: - data["image_url"] = None - data["thumbnail_url"] = None - if relationships := asset.get("relationships"): + if notes := asset["attributes"]["notes"]: + notes = notes["value"] - if image := relationships.get("image"): - if image["data"]: - image = self.farmos_client.resource.get_id( - "file", "file", image["data"][0]["id"] - ) - if image_style := image["data"]["attributes"].get( - "image_style_uri" - ): - data["image_url"] = image_style["large"] - data["thumbnail_url"] = image_style["thumbnail"] + if self.farmos_4x: + archived = asset["attributes"]["archived"] + else: + archived = asset["attributes"]["status"] == "archived" + parents = None if "parents" in self.fields: - data["parents"] = [] + parents = [] for parent in asset["relationships"]["parent"]["data"]: - data["parents"].append( - (self.get_asset_type(parent), UUID(parent["id"])) - ) + parents.append((self.get_asset_type(parent), UUID(parent["id"]))) - if "owners" in self.fields: - data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]] - - return data + return { + "farmos_uuid": UUID(asset["id"]), + "drupal_id": asset["attributes"]["drupal_internal__id"], + "asset_name": asset["attributes"]["name"], + "is_location": asset["attributes"]["is_location"], + "is_fixed": asset["attributes"]["is_fixed"], + "archived": archived, + "notes": notes, + "image_url": image_url, + "thumbnail_url": thumbnail_url, + "parents": parents, + } def get_asset_type(self, asset): return asset["type"].split("--")[1] @@ -252,10 +256,10 @@ class AssetImporterBase(FromFarmOS, ToWutta): data = super().normalize_target_object(asset) if "parents" in self.fields: - data["parents"] = [(p.asset_type, p.farmos_uuid) for p in asset.parents] - - if "owners" in self.fields: - data["owners"] = [user.farmos_uuid for user in asset.owners] + data["parents"] = [ + (p.parent.asset_type, p.parent.farmos_uuid) + for p in asset.asset._parents + ] return data @@ -296,30 +300,6 @@ class AssetImporterBase(FromFarmOS, ToWutta): ) self.target_session.delete(parent) - if "owners" in self.fields: - if not target_data or target_data["owners"] != source_data["owners"]: - - for farmos_uuid in source_data["owners"]: - if not target_data or farmos_uuid not in target_data["owners"]: - user = ( - self.target_session.query(model.User) - .join(model.WuttaFarmUser) - .filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid) - .one() - ) - asset.owners.append(user) - - if target_data: - for farmos_uuid in target_data["owners"]: - if farmos_uuid not in source_data["owners"]: - user = ( - self.target_session.query(model.User) - .join(model.WuttaFarmUser) - .filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid) - .one() - ) - asset.owners.remove(user) - return asset @@ -355,6 +335,11 @@ class AnimalAssetImporter(AssetImporterBase): if animal_type.farmos_uuid: self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type + def get_source_objects(self): + """ """ + animals = self.farmos_client.asset.get("animal") + return animals["data"] + def normalize_source_object(self, animal): """ """ animal_type_uuid = None @@ -386,9 +371,10 @@ class AnimalAssetImporter(AssetImporterBase): else: sterile = animal["attributes"]["is_castrated"] - data = super().normalize_source_object(animal) + data = self.normalize_asset(animal) data.update( { + "asset_type": "animal", "animal_type_uuid": animal_type_uuid, "sex": animal["attributes"]["sex"], "is_sterile": sterile, @@ -479,11 +465,17 @@ class GroupAssetImporter(AssetImporterBase): "parents", ] + def get_source_objects(self): + """ """ + groups = self.farmos_client.asset.get("group") + return groups["data"] + def normalize_source_object(self, group): """ """ - data = super().normalize_source_object(group) + data = self.normalize_asset(group) data.update( { + "asset_type": "group", "produces_eggs": group["attributes"]["produces_eggs"], } ) @@ -519,6 +511,11 @@ 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_source_objects(self): + """ """ + land_assets = self.farmos_client.asset.get("land") + return land_assets["data"] + def normalize_source_object(self, land): """ """ land_type_id = land["attributes"]["land_type"] @@ -529,9 +526,10 @@ class LandAssetImporter(AssetImporterBase): ) return None - data = super().normalize_source_object(land) + data = self.normalize_asset(land) data.update( { + "asset_type": "land", "land_type_uuid": land_type.uuid, } ) @@ -624,7 +622,7 @@ class PlantAssetImporter(AssetImporterBase): def normalize_source_object(self, plant): """ """ - plant_types = [] + plant_types = None if relationships := plant.get("relationships"): if plant_type := relationships.get("plant_type"): @@ -637,10 +635,11 @@ class PlantAssetImporter(AssetImporterBase): else: log.warning("plant type not found: %s", plant_type["id"]) - data = super().normalize_source_object(plant) + data = self.normalize_asset(plant) data.update( { - "plant_types": set(plant_types), + "asset_type": "plant", + "plant_types": plant_types, } ) return data @@ -649,7 +648,7 @@ class PlantAssetImporter(AssetImporterBase): data = super().normalize_target_object(plant) if "plant_types" in self.fields: - data["plant_types"] = set([pt.uuid for pt in plant.plant_types]) + data["plant_types"] = [t.plant_type_uuid for t in plant._plant_types] return data @@ -716,6 +715,11 @@ 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_source_objects(self): + """ """ + structures = self.farmos_client.asset.get("structure") + return structures["data"] + def normalize_source_object(self, structure): """ """ structure_type_id = structure["attributes"]["structure_type"] @@ -728,9 +732,10 @@ class StructureAssetImporter(AssetImporterBase): ) return None - data = super().normalize_source_object(structure) + data = self.normalize_asset(structure) data.update( { + "asset_type": "structure", "structure_type_uuid": structure_type.uuid, } ) @@ -959,11 +964,8 @@ class LogImporterBase(FromFarmOS, ToWutta): "log_type", "message", "timestamp", - "is_movement", - "is_group_assignment", "notes", "status", - "quick", ] ) return fields @@ -974,10 +976,6 @@ class LogImporterBase(FromFarmOS, ToWutta): fields.extend( [ "assets", - "groups", - "locations", - "quantities", - "owners", ] ) return fields @@ -994,7 +992,6 @@ class LogImporterBase(FromFarmOS, ToWutta): data["farmos_uuid"] = UUID(data.pop("uuid")) data["message"] = data.pop("name") data["timestamp"] = self.app.make_utc(data["timestamp"]) - data["quick"] = ", ".join(data["quick"]) if data["quick"] else None # TODO data["log_type"] = self.get_farmos_log_type() @@ -1004,23 +1001,6 @@ class LogImporterBase(FromFarmOS, ToWutta): (a["asset_type"], UUID(a["uuid"])) for a in data["assets"] ] - if "groups" in self.fields: - data["groups"] = [ - (asset["asset_type"], UUID(asset["uuid"])) for asset in data["groups"] - ] - - if "locations" in self.fields: - data["locations"] = [ - (asset["asset_type"], UUID(asset["uuid"])) - for asset in data["locations"] - ] - - if "quantities" in self.fields: - data["quantities"] = [UUID(uuid) for uuid in data["quantity_uuids"]] - - if "owners" in self.fields: - data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]] - return data def normalize_target_object(self, log): @@ -1028,25 +1008,9 @@ class LogImporterBase(FromFarmOS, ToWutta): if "assets" in self.fields: data["assets"] = [ - (asset.asset_type, asset.farmos_uuid) for asset in log.assets + (a.asset.asset_type, a.asset.farmos_uuid) for a in log.log._assets ] - if "groups" in self.fields: - data["groups"] = [ - (asset.asset_type, asset.farmos_uuid) for asset in log.groups - ] - - if "locations" in self.fields: - data["locations"] = [ - (asset.asset_type, asset.farmos_uuid) for asset in log.locations - ] - - if "quantities" in self.fields: - data["quantities"] = [qty.farmos_uuid for qty in log.quantities] - - if "owners" in self.fields: - data["owners"] = [user.farmos_uuid for user in log.owners] - return data def update_target_object(self, log, source_data, target_data=None): @@ -1059,13 +1023,14 @@ class LogImporterBase(FromFarmOS, ToWutta): for key in source_data["assets"]: asset_type, farmos_uuid = key if not target_data or key not in target_data["assets"]: + self.target_session.flush() asset = ( self.target_session.query(model.Asset) .filter(model.Asset.asset_type == asset_type) .filter(model.Asset.farmos_uuid == farmos_uuid) .one() ) - log.assets.append(asset) + log.log._assets.append(model.LogAsset(asset=asset)) if target_data: for key in target_data["assets"]: @@ -1077,108 +1042,13 @@ class LogImporterBase(FromFarmOS, ToWutta): .filter(model.Asset.farmos_uuid == farmos_uuid) .one() ) - log.assets.remove(asset) - - if "groups" in self.fields: - if not target_data or target_data["groups"] != source_data["groups"]: - - for key in source_data["groups"]: - asset_type, farmos_uuid = key - if not target_data or key not in target_data["groups"]: - asset = ( - self.target_session.query(model.Asset) - .filter(model.Asset.asset_type == asset_type) - .filter(model.Asset.farmos_uuid == farmos_uuid) - .one() - ) - log.groups.append(asset) - - if target_data: - for key in target_data["groups"]: - asset_type, farmos_uuid = key - if key not in source_data["groups"]: asset = ( - self.target_session.query(model.Asset) - .filter(model.Asset.asset_type == asset_type) - .filter(model.Asset.farmos_uuid == farmos_uuid) + self.target_session.query(model.LogAsset) + .filter(model.LogAsset.log == log) + .filter(model.LogAsset.asset == asset) .one() ) - log.groups.remove(asset) - - if "locations" in self.fields: - if not target_data or target_data["locations"] != source_data["locations"]: - - for key in source_data["locations"]: - asset_type, farmos_uuid = key - if not target_data or key not in target_data["locations"]: - asset = ( - self.target_session.query(model.Asset) - .filter(model.Asset.asset_type == asset_type) - .filter(model.Asset.farmos_uuid == farmos_uuid) - .one() - ) - log.locations.append(asset) - - if target_data: - for key in target_data["locations"]: - asset_type, farmos_uuid = key - if key not in source_data["locations"]: - asset = ( - self.target_session.query(model.Asset) - .filter(model.Asset.asset_type == asset_type) - .filter(model.Asset.farmos_uuid == farmos_uuid) - .one() - ) - log.locations.remove(asset) - - if "quantities" in self.fields: - if ( - not target_data - or target_data["quantities"] != source_data["quantities"] - ): - - for farmos_uuid in source_data["quantities"]: - if not target_data or farmos_uuid not in target_data["quantities"]: - qty = ( - self.target_session.query(model.Quantity) - .filter(model.Quantity.farmos_uuid == farmos_uuid) - .one() - ) - log.quantities.append(qty) - - if target_data: - for farmos_uuid in target_data["quantities"]: - if farmos_uuid not in source_data["quantities"]: - qty = ( - self.target_session.query(model.Quantity) - .filter(model.Quantity.farmos_uuid == farmos_uuid) - .one() - ) - log.quantities.remove(qty) - - if "owners" in self.fields: - if not target_data or target_data["owners"] != source_data["owners"]: - - for farmos_uuid in source_data["owners"]: - if not target_data or farmos_uuid not in target_data["owners"]: - user = ( - self.target_session.query(model.User) - .join(model.WuttaFarmUser) - .filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid) - .one() - ) - log.owners.append(user) - - if target_data: - for farmos_uuid in target_data["owners"]: - if farmos_uuid not in source_data["owners"]: - user = ( - self.target_session.query(model.User) - .join(model.WuttaFarmUser) - .filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid) - .one() - ) - log.owners.remove(user) + self.target_session.delete(asset) return log @@ -1190,6 +1060,17 @@ class ActivityLogImporter(LogImporterBase): model_class = model.ActivityLog + supported_fields = [ + "farmos_uuid", + "drupal_id", + "log_type", + "message", + "timestamp", + "notes", + "status", + "assets", + ] + class HarvestLogImporter(LogImporterBase): """ @@ -1198,6 +1079,17 @@ class HarvestLogImporter(LogImporterBase): model_class = model.HarvestLog + supported_fields = [ + "farmos_uuid", + "drupal_id", + "log_type", + "message", + "timestamp", + "notes", + "status", + "assets", + ] + class MedicalLogImporter(LogImporterBase): """ @@ -1206,16 +1098,16 @@ class MedicalLogImporter(LogImporterBase): model_class = model.MedicalLog - def get_simple_fields(self): - """ """ - fields = list(super().get_simple_fields()) - # nb. must explicitly declare proxy fields - fields.extend( - [ - "vet", - ] - ) - return fields + supported_fields = [ + "farmos_uuid", + "drupal_id", + "log_type", + "message", + "timestamp", + "notes", + "status", + "assets", + ] class ObservationLogImporter(LogImporterBase): @@ -1225,6 +1117,17 @@ class ObservationLogImporter(LogImporterBase): model_class = model.ObservationLog + supported_fields = [ + "farmos_uuid", + "drupal_id", + "log_type", + "message", + "timestamp", + "notes", + "status", + "assets", + ] + class QuantityImporterBase(FromFarmOS, ToWutta): """ diff --git a/src/wuttafarm/normal.py b/src/wuttafarm/normal.py index 4fc8796..ca7be39 100644 --- a/src/wuttafarm/normal.py +++ b/src/wuttafarm/normal.py @@ -84,40 +84,6 @@ class Normalizer(GenericHandler): self._farmos_units = units return self._farmos_units - def normalize_farmos_asset(self, asset, included={}): - """ """ - - if notes := asset["attributes"]["notes"]: - notes = notes["value"] - - owner_objects = [] - owner_uuids = [] - if relationships := asset.get("relationships"): - - if owners := relationships.get("owner"): - for user in owners["data"]: - user_uuid = user["id"] - owner_uuids.append(user_uuid) - if user := included.get(user_uuid): - owner_objects.append( - { - "uuid": user["id"], - "name": user["attributes"]["name"], - } - ) - - return { - "uuid": asset["id"], - "drupal_id": asset["attributes"]["drupal_internal__id"], - "asset_name": asset["attributes"]["name"], - "is_location": asset["attributes"]["is_location"], - "is_fixed": asset["attributes"]["is_fixed"], - "archived": asset["attributes"]["archived"], - "notes": notes, - "owners": owner_objects, - "owner_uuids": owner_uuids, - } - def normalize_farmos_log(self, log, included={}): if timestamp := log["attributes"]["timestamp"]: @@ -130,12 +96,8 @@ class Normalizer(GenericHandler): log_type_object = {} log_type_uuid = None asset_objects = [] - group_objects = [] - group_uuids = [] quantity_objects = [] quantity_uuids = [] - location_objects = [] - location_uuids = [] owner_objects = [] owner_uuids = [] if relationships := log.get("relationships"): @@ -170,54 +132,6 @@ class Normalizer(GenericHandler): ) asset_objects.append(asset_object) - if groups := relationships.get("group"): - for group in groups["data"]: - group_uuid = group["id"] - group_uuids.append(group_uuid) - group_object = { - "uuid": group["id"], - "type": group["type"], - "asset_type": group["type"].split("--")[1], - } - if group := included.get(group_uuid): - attrs = group["attributes"] - rels = group["relationships"] - group_object.update( - { - "drupal_id": attrs["drupal_internal__id"], - "name": attrs["name"], - "is_location": attrs["is_location"], - "is_fixed": attrs["is_fixed"], - "archived": attrs["archived"], - "notes": attrs["notes"], - } - ) - group_objects.append(group_object) - - if locations := relationships.get("location"): - for location in locations["data"]: - location_uuid = location["id"] - location_uuids.append(location_uuid) - location_object = { - "uuid": location["id"], - "type": location["type"], - "asset_type": location["type"].split("--")[1], - } - if location := included.get(location_uuid): - attrs = location["attributes"] - rels = location["relationships"] - location_object.update( - { - "drupal_id": attrs["drupal_internal__id"], - "name": attrs["name"], - "is_location": attrs["is_location"], - "is_fixed": attrs["is_fixed"], - "archived": attrs["archived"], - "notes": attrs["notes"], - } - ) - location_objects.append(location_object) - if quantities := relationships.get("quantity"): for quantity in quantities["data"]: quantity_uuid = quantity["id"] @@ -274,19 +188,12 @@ class Normalizer(GenericHandler): "name": log["attributes"]["name"], "timestamp": timestamp, "assets": asset_objects, - "groups": group_objects, - "group_uuids": group_uuids, "quantities": quantity_objects, "quantity_uuids": quantity_uuids, "is_group_assignment": log["attributes"]["is_group_assignment"], - "is_movement": log["attributes"]["is_movement"], "quick": log["attributes"]["quick"], "status": log["attributes"]["status"], "notes": notes, - "locations": location_objects, - "location_uuids": location_uuids, "owners": owner_objects, "owner_uuids": owner_uuids, - # TODO: should we do this here or make caller do it? - "vet": log["attributes"].get("vet"), } diff --git a/src/wuttafarm/util.py b/src/wuttafarm/util.py deleted file mode 100644 index 1700998..0000000 --- a/src/wuttafarm/util.py +++ /dev/null @@ -1,37 +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 . -# -################################################################################ -""" -misc. utilities -""" - -from collections import OrderedDict - - -def get_log_type_enum(config, session=None): - app = config.get_app() - model = app.model - log_types = OrderedDict() - with app.short_session(session=session) as sess: - query = sess.query(model.LogType).order_by(model.LogType.name) - for log_type in query: - log_types[log_type.drupal_id] = log_type.name - return log_types diff --git a/src/wuttafarm/web/app.py b/src/wuttafarm/web/app.py index 161a876..2fcb48d 100644 --- a/src/wuttafarm/web/app.py +++ b/src/wuttafarm/web/app.py @@ -40,15 +40,6 @@ def main(global_config, **settings): "wuttaweb:templates", ], ) - settings.setdefault( - "pyramid_deform.template_search_path", - " ".join( - [ - "wuttafarm.web:templates/deform", - "wuttaweb:templates/deform", - ] - ), - ) # make config objects wutta_config = base.make_wutta_config(settings) diff --git a/src/wuttafarm/web/forms/schema.py b/src/wuttafarm/web/forms/schema.py index 6bf434e..075c36c 100644 --- a/src/wuttafarm/web/forms/schema.py +++ b/src/wuttafarm/web/forms/schema.py @@ -27,7 +27,6 @@ import json import colander -from wuttaweb.db import Session from wuttaweb.forms.schema import ObjectRef, WuttaSet from wuttaweb.forms.widgets import NotesWidget @@ -56,12 +55,6 @@ class AnimalTypeRef(ObjectRef): animal_type = obj return self.request.route_url("animal_types.view", uuid=animal_type.uuid) - def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import AnimalTypeRefWidget - - kwargs["factory"] = AnimalTypeRefWidget - return super().widget_maker(**kwargs) - class LogQuick(WuttaSet): @@ -77,31 +70,6 @@ class LogQuick(WuttaSet): return LogQuickWidget(**kwargs) -class LogRef(ObjectRef): - """ - Custom schema type for a - :class:`~wuttafarm.db.model.log.Log` reference field. - - This is a subclass of - :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`. - """ - - @property - def model_class(self): # pylint: disable=empty-docstring - """ """ - model = self.app.model - return model.Log - - def sort_query(self, query): # pylint: disable=empty-docstring - """ """ - return query.order_by(self.model_class.message) - - def get_object_url(self, obj): # pylint: disable=empty-docstring - """ """ - log = obj - return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) - - class FarmOSUnitRef(colander.SchemaType): def serialize(self, node, appstruct): @@ -217,6 +185,25 @@ class FarmOSQuantityRefs(WuttaSet): return FarmOSQuantityRefsWidget(**kwargs) +class AnimalTypeType(colander.SchemaType): + + def __init__(self, request, *args, **kwargs): + super().__init__(*args, **kwargs) + self.request = request + + def serialize(self, node, appstruct): + if appstruct is colander.null: + return colander.null + + return json.dumps(appstruct) + + def widget_maker(self, **kwargs): # pylint: disable=empty-docstring + """ """ + from wuttafarm.web.forms.widgets import AnimalTypeWidget + + return AnimalTypeWidget(self.request, **kwargs) + + class FarmOSPlantTypes(colander.SchemaType): def __init__(self, request, *args, **kwargs): @@ -268,23 +255,13 @@ class PlantTypeRefs(WuttaSet): def serialize(self, node, appstruct): if not appstruct: - return colander.null - - return [uuid.hex for uuid in appstruct] + appstruct = [] + uuids = [u.hex for u in appstruct] + return json.dumps(uuids) def widget_maker(self, **kwargs): from wuttafarm.web.forms.widgets import PlantTypeRefsWidget - model = self.app.model - session = Session() - - if "values" not in kwargs: - plant_types = ( - session.query(model.PlantType).order_by(model.PlantType.name).all() - ) - values = [(pt.uuid.hex, str(pt)) for pt in plant_types] - kwargs["values"] = values - return PlantTypeRefsWidget(self.request, **kwargs) @@ -389,55 +366,21 @@ class AssetParentRefs(WuttaSet): return AssetParentRefsWidget(self.request, **kwargs) -class AssetRefs(WuttaSet): +class LogAssetRefs(WuttaSet): """ Schema type for Assets field (on a Log record) """ def serialize(self, node, appstruct): if not appstruct: - return colander.null - - return {asset.uuid for asset in appstruct} + appstruct = [] + uuids = [u.hex for u in appstruct] + return json.dumps(uuids) def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import AssetRefsWidget + from wuttafarm.web.forms.widgets import LogAssetRefsWidget - return AssetRefsWidget(self.request, **kwargs) - - -class LogQuantityRefs(WuttaSet): - """ - Schema type for Quantities field (on a Log record) - """ - - def serialize(self, node, appstruct): - if not appstruct: - return colander.null - - return {qty.uuid for qty in appstruct} - - def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import LogQuantityRefsWidget - - return LogQuantityRefsWidget(self.request, **kwargs) - - -class OwnerRefs(WuttaSet): - """ - Schema type for Owners field (on a Log record) - """ - - def serialize(self, node, appstruct): - if not appstruct: - return colander.null - - return {user.uuid for user in appstruct} - - def widget_maker(self, **kwargs): - from wuttafarm.web.forms.widgets import OwnerRefsWidget - - return OwnerRefsWidget(self.request, **kwargs) + return LogAssetRefsWidget(self.request, **kwargs) class Notes(colander.String): diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py index 0a14638..9dcc51f 100644 --- a/src/wuttafarm/web/forms/widgets.py +++ b/src/wuttafarm/web/forms/widgets.py @@ -26,10 +26,10 @@ Custom form widgets for WuttaFarm import json import colander -from deform.widget import Widget, SelectWidget, sequence_types, _normalize_choices +from deform.widget import Widget, SelectWidget from webhelpers2.html import HTML, tags -from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget +from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget from wuttaweb.db import Session from wuttafarm.web.util import render_quantity_objects @@ -228,6 +228,33 @@ class FarmOSUnitRefWidget(Widget): return super().serialize(field, cstruct, **kw) +class AnimalTypeWidget(Widget): + """ + Widget to display an "animal type" field. + """ + + def __init__(self, request, *args, **kwargs): + super().__init__(*args, **kwargs) + self.request = request + + def serialize(self, field, cstruct, **kw): + """ """ + readonly = kw.get("readonly", self.readonly) + if readonly: + if cstruct in (colander.null, None): + return HTML.tag("span") + + animal_type = json.loads(cstruct) + return tags.link_to( + animal_type["name"], + self.request.route_url( + "farmos_animal_types.view", uuid=animal_type["uuid"] + ), + ) + + return super().serialize(field, cstruct, **kw) + + class FarmOSPlantTypesWidget(Widget): """ Widget to display a farmOS "plant types" field. @@ -258,40 +285,22 @@ class FarmOSPlantTypesWidget(Widget): return super().serialize(field, cstruct, **kw) -class PlantTypeRefsWidget(Widget): +class PlantTypeRefsWidget(WuttaCheckboxChoiceWidget): """ Widget for Plant Types field (on a Plant Asset). """ - template = "planttyperefs" - 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 = [] - - plant_types = ( - session.query(model.PlantType) - .filter(model.PlantType.uuid.in_(cstruct)) - .order_by(model.PlantType.name) - .all() - ) - - for plant_type in plant_types: - items.append( + readonly = kw.get("readonly", self.readonly) + if readonly: + plant_types = [] + for uuid in json.loads(cstruct): + plant_type = session.get(model.PlantType, uuid) + plant_types.append( HTML.tag( "li", c=tags.link_to( @@ -302,34 +311,9 @@ class PlantTypeRefsWidget(Widget): ), ) ) + return HTML.tag("ul", c=plant_types) - 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("plant_types.create"): - values["can_create"] = True - - return values - - def deserialize(self, field, pstruct): - """ """ - if not pstruct: - return colander.null - - return set(pstruct.split(",")) + return super().serialize(field, cstruct, **kw) class StructureWidget(Widget): @@ -388,11 +372,6 @@ class UsersWidget(Widget): return super().serialize(field, cstruct, **kw) -############################## -# native data widgets -############################## - - class AssetParentRefsWidget(WuttaCheckboxChoiceWidget): """ Widget for Parents field which references assets. @@ -424,9 +403,9 @@ class AssetParentRefsWidget(WuttaCheckboxChoiceWidget): return super().serialize(field, cstruct, **kw) -class AssetRefsWidget(WuttaCheckboxChoiceWidget): +class LogAssetRefsWidget(WuttaCheckboxChoiceWidget): """ - Widget for Assets field (of various kinds). + Widget for Assets field (on a Log record) """ def serialize(self, field, cstruct, **kw): @@ -437,7 +416,7 @@ class AssetRefsWidget(WuttaCheckboxChoiceWidget): readonly = kw.get("readonly", self.readonly) if readonly: assets = [] - for uuid in cstruct or []: + for uuid in json.loads(cstruct): asset = session.get(model.Asset, uuid) assets.append( HTML.tag( @@ -453,85 +432,3 @@ class AssetRefsWidget(WuttaCheckboxChoiceWidget): return HTML.tag("ul", c=assets) return super().serialize(field, cstruct, **kw) - - -class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget): - """ - Widget for Quantities field (on a Log record) - """ - - def serialize(self, field, cstruct, **kw): - """ """ - model = self.app.model - session = Session() - - readonly = kw.get("readonly", self.readonly) - if readonly: - quantities = [] - 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 - ), - ), - ) - ) - return HTML.tag("ul", c=quantities) - - return super().serialize(field, cstruct, **kw) - - -class OwnerRefsWidget(WuttaCheckboxChoiceWidget): - """ - Widget for Owners field (on an Asset or Log record) - """ - - def serialize(self, field, cstruct, **kw): - """ """ - model = self.app.model - session = Session() - - readonly = kw.get("readonly", self.readonly) - if readonly: - owners = [session.get(model.User, uuid) for uuid in cstruct or []] - owners = [user for user in owners if user] - owners.sort(key=lambda user: user.username) - links = [] - for user in owners: - links.append( - HTML.tag( - "li", - c=tags.link_to( - user.username, - self.request.route_url("users.view", uuid=user.uuid), - ), - ) - ) - return HTML.tag("ul", c=links) - - return super().serialize(field, cstruct, **kw) - - -class AnimalTypeRefWidget(ObjectRefWidget): - """ - Custom widget which uses the ```` component. - """ - - template = "animaltyperef" - - 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("animal_types.create"): - values["can_create"] = True - - return values diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index fe7719e..6ce4a8d 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -72,7 +72,7 @@ class WuttaFarmMenuHandler(base.MenuHandler): { "title": "Eggs", "route": "quick.eggs", - "perm": "quick.eggs", + # "perm": "assets.list", }, ], } diff --git a/src/wuttafarm/web/templates/appinfo/configure.mako b/src/wuttafarm/web/templates/appinfo/configure.mako index 912eef0..3760577 100644 --- a/src/wuttafarm/web/templates/appinfo/configure.mako +++ b/src/wuttafarm/web/templates/appinfo/configure.mako @@ -14,47 +14,14 @@ - - - - - - - - - - - - - - - - - - -
- - % for value, label in enum.FARMOS_INTEGRATION_MODE.items(): - - % endfor - - <${b}-tooltip position="${'right' if request.use_oruga else 'is-right'}"> - - - -
+ + % for value, label in enum.FARMOS_INTEGRATION_MODE.items(): + + % endfor +
-<%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" /> <%def name="index_title_controls()"> ${parent.index_title_controls()} @@ -15,8 +14,3 @@ % endif - -<%def name="render_vue_templates()"> - ${parent.render_vue_templates()} - ${make_wuttafarm_components()} - diff --git a/src/wuttafarm/web/templates/deform/animaltyperef.pt b/src/wuttafarm/web/templates/deform/animaltyperef.pt deleted file mode 100644 index 61dd770..0000000 --- a/src/wuttafarm/web/templates/deform/animaltyperef.pt +++ /dev/null @@ -1,13 +0,0 @@ -
- - - -
diff --git a/src/wuttafarm/web/templates/deform/planttyperefs.pt b/src/wuttafarm/web/templates/deform/planttyperefs.pt deleted file mode 100644 index 83cb095..0000000 --- a/src/wuttafarm/web/templates/deform/planttyperefs.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 deleted file mode 100644 index 37b176e..0000000 --- a/src/wuttafarm/web/templates/wuttafarm-components.mako +++ /dev/null @@ -1,324 +0,0 @@ - -<%def name="make_wuttafarm_components()"> - ${self.make_animal_type_picker_component()} - ${self.make_plant_types_picker_component()} - - -<%def name="make_animal_type_picker_component()"> - - - - -<%def name="make_plant_types_picker_component()"> - - - diff --git a/src/wuttafarm/web/util.py b/src/wuttafarm/web/util.py index 977550a..2d51851 100644 --- a/src/wuttafarm/web/util.py +++ b/src/wuttafarm/web/util.py @@ -23,29 +23,9 @@ Misc. utilities for web app """ -from pyramid import httpexceptions from webhelpers2.html import HTML -def get_farmos_client_for_user(request): - token = request.session.get("farmos.oauth2.token") - if not token: - raise httpexceptions.HTTPForbidden() - - # nb. must give a *copy* of the token to farmOS client, since it - # will mutate it in-place and we don't want that to happen for our - # original copy in the user session. (otherwise the auto-refresh - # will not work correctly for subsequent calls.) - token = dict(token) - - def token_updater(token): - save_farmos_oauth2_token(request, token) - - config = request.wutta_config - app = config.get_app() - return app.get_farmos_client(token=token, token_updater=token_updater) - - def save_farmos_oauth2_token(request, token): """ Common logic for saving the given OAuth2 token within the user diff --git a/src/wuttafarm/web/views/animals.py b/src/wuttafarm/web/views/animals.py index f4c97e2..76e0335 100644 --- a/src/wuttafarm/web/views/animals.py +++ b/src/wuttafarm/web/views/animals.py @@ -26,13 +26,11 @@ Master view for Animals from webhelpers2.html import tags from wuttaweb.forms.schema import WuttaDictEnum -from wuttaweb.util import get_form_data from wuttafarm.db.model import AnimalType, AnimalAsset from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView from wuttafarm.web.forms.schema import AnimalTypeRef from wuttafarm.web.forms.widgets import ImageWidget -from wuttafarm.web.util import get_farmos_client_for_user class AnimalTypeView(AssetTypeMasterView): @@ -44,8 +42,6 @@ class AnimalTypeView(AssetTypeMasterView): route_prefix = "animal_types" url_prefix = "/animal-types" - farmos_entity_type = "taxonomy_term" - farmos_bundle = "animal_type" farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview" grid_columns = [ @@ -62,8 +58,8 @@ class AnimalTypeView(AssetTypeMasterView): form_fields = [ "name", "description", - "drupal_id", "farmos_uuid", + "drupal_id", ] has_rows = True @@ -107,19 +103,6 @@ class AnimalTypeView(AssetTypeMasterView): return buttons - def delete(self): - animal_type = self.get_instance() - - if animal_type.animal_assets: - self.request.session.flash( - "Cannot delete animal type which is still referenced by animal assets.", - "warning", - ) - url = self.get_action_url("view", animal_type) - return self.redirect(self.request.get_referrer(default=url)) - - return super().delete() - def get_row_grid_data(self, animal_type): model = self.app.model session = self.Session() @@ -142,7 +125,6 @@ class AnimalTypeView(AssetTypeMasterView): # sex g.set_enum("sex", enum.ANIMAL_SEX) - g.filters["sex"].verbs = ["equal", "not_equal"] # archived g.set_renderer("archived", "boolean") @@ -152,55 +134,6 @@ class AnimalTypeView(AssetTypeMasterView): def get_row_action_url_view(self, animal, i): return self.request.route_url("animal_assets.view", uuid=animal.uuid) - def ajax_create(self): - """ - AJAX view to create a new animal type. - """ - 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"} - - animal_type = model.AnimalType(name=name) - session.add(animal_type) - session.flush() - - if self.app.is_farmos_mirror(): - client = get_farmos_client_for_user(self.request) - self.app.auto_sync_to_farmos(animal_type, client=client) - - return { - "uuid": animal_type.uuid.hex, - "name": animal_type.name, - "farmos_uuid": animal_type.farmos_uuid.hex, - "drupal_id": animal_type.drupal_id, - } - - @classmethod - def defaults(cls, config): - """ """ - cls._defaults(config) - cls._animal_type_defaults(config) - - @classmethod - def _animal_type_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 AnimalAssetView(AssetMasterView): """ @@ -212,10 +145,9 @@ class AnimalAssetView(AssetMasterView): url_prefix = "/assets/animal" farmos_refurl_path = "/assets/animal" - farmos_bundle = "animal" labels = { - "animal_type": "Species / Breed", + "animal_type": "Species/Breed", "is_sterile": "Sterile", } @@ -228,9 +160,6 @@ class AnimalAssetView(AssetMasterView): "birthdate", "is_sterile", "sex", - "groups", - "owners", - "locations", "archived", ] @@ -243,12 +172,9 @@ class AnimalAssetView(AssetMasterView): "is_sterile", "notes", "asset_type", - "owners", - "locations", - "groups", "archived", - "drupal_id", "farmos_uuid", + "drupal_id", "thumbnail_url", "image_url", "thumbnail", @@ -275,7 +201,6 @@ class AnimalAssetView(AssetMasterView): # sex g.set_enum("sex", enum.ANIMAL_SEX) - g.filters["sex"].verbs = ["equal", "not_equal"] def render_animal_type_for_grid(self, animal, field, value): url = self.request.route_url("animal_types.view", uuid=animal.animal_type_uuid) @@ -291,7 +216,7 @@ class AnimalAssetView(AssetMasterView): f.set_node("animal_type", AnimalTypeRef(self.request)) # sex - if not (self.creating or self.editing) and animal.sex is None: + if self.viewing and animal.sex is None: pass # TODO: dict enum widget does not handle null values well else: f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX)) diff --git a/src/wuttafarm/web/views/asset_types.py b/src/wuttafarm/web/views/asset_types.py index 2fb0239..b9f560a 100644 --- a/src/wuttafarm/web/views/asset_types.py +++ b/src/wuttafarm/web/views/asset_types.py @@ -38,7 +38,6 @@ class AssetTypeView(WuttaFarmMasterView): grid_columns = [ "name", - "drupal_id", "description", ] @@ -51,8 +50,8 @@ class AssetTypeView(WuttaFarmMasterView): form_fields = [ "name", "description", - "drupal_id", "farmos_uuid", + "drupal_id", ] def configure_grid(self, grid): @@ -79,19 +78,6 @@ class AssetTypeView(WuttaFarmMasterView): return buttons - @classmethod - def defaults(cls, config): - """ """ - wutta_config = config.registry.settings.get("wutta_config") - app = wutta_config.get_app() - - if app.is_farmos_mirror(): - cls.creatable = False - cls.editable = False - cls.deletable = False - - cls._defaults(config) - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/assets.py b/src/wuttafarm/web/views/assets.py index b4e4d31..b78f149 100644 --- a/src/wuttafarm/web/views/assets.py +++ b/src/wuttafarm/web/views/assets.py @@ -25,17 +25,13 @@ Master view for Assets from collections import OrderedDict -from webhelpers2.html import tags - from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.db import Session from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.db.model import Asset, Log -from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs +from wuttafarm.web.forms.schema import AssetParentRefs 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 def get_asset_type_enum(config): @@ -49,6 +45,79 @@ def get_asset_type_enum(config): return asset_types +class AssetView(WuttaFarmMasterView): + """ + Master view for Assets + """ + + model_class = Asset + route_prefix = "assets" + url_prefix = "/assets" + + farmos_refurl_path = "/assets" + + viewable = False + creatable = False + editable = False + deletable = False + model_is_versioned = False + + grid_columns = [ + "thumbnail", + "drupal_id", + "asset_name", + "asset_type", + "parents", + "archived", + ] + + sort_defaults = "asset_name" + + filter_defaults = { + "asset_name": {"active": True, "verb": "contains"}, + "archived": {"active": True, "verb": "is_false"}, + } + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # thumbnail + g.set_renderer("thumbnail", self.render_grid_thumbnail) + g.set_label("thumbnail", "", column_only=True) + g.set_centered("thumbnail") + + # drupal_id + g.set_label("drupal_id", "ID", column_only=True) + + # asset_name + g.set_link("asset_name") + + # asset_type + g.set_enum("asset_type", get_asset_type_enum(self.config)) + + # parents + g.set_renderer("parents", self.render_parents_for_grid) + + # view action links to final asset record + def asset_url(asset, i): + return self.request.route_url( + f"{asset.asset_type}_assets.view", uuid=asset.uuid + ) + + g.add_action("view", icon="eye", url=asset_url) + + def render_parents_for_grid(self, asset, field, value): + parents = [str(p.parent) for p in asset._parents] + return ", ".join(parents) + + def grid_row_class(self, asset, data, i): + """ """ + if asset.archived: + return "has-background-warning" + return None + + class AssetTypeMasterView(WuttaFarmMasterView): """ Base class for "Asset Type" master views. @@ -64,12 +133,6 @@ class AssetMasterView(WuttaFarmMasterView): Base class for Asset master views """ - farmos_entity_type = "asset" - - labels = { - "groups": "Group Membership", - } - sort_defaults = "asset_name" filter_defaults = { @@ -112,10 +175,7 @@ class AssetMasterView(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.Asset: - query = query.join(model.Asset) - return query + return session.query(model_class).join(model.Asset) def configure_grid(self, grid): g = grid @@ -140,77 +200,15 @@ class AssetMasterView(WuttaFarmMasterView): # parents g.set_renderer("parents", self.render_parents_for_grid) - # groups - g.set_renderer("groups", self.render_groups_for_grid) - - # owners - g.set_label("owners", "Owner") - g.set_renderer("owners", self.render_owners_for_grid) - - # locations - g.set_label("locations", "Location") - g.set_renderer("locations", self.render_locations_for_grid) - # archived g.set_renderer("archived", "boolean") g.set_sorter("archived", model.Asset.archived) g.set_filter("archived", model.Asset.archived) def render_parents_for_grid(self, asset, field, value): - - if self.farmos_style_grid_links: - links = [] - for parent in asset.parents: - url = self.request.route_url( - f"{parent.asset_type}_assets.view", uuid=parent.uuid - ) - links.append(tags.link_to(str(parent), url)) - return ", ".join(links) - - parents = [str(p.parent) for p in asset.parents] + parents = [str(p.parent) for p in asset.asset._parents] return ", ".join(parents) - def render_owners_for_grid(self, asset, field, value): - - if self.farmos_style_grid_links: - links = [] - for user in asset.owners: - url = self.request.route_url("users.view", uuid=user.uuid) - links.append(tags.link_to(user.username, url)) - return ", ".join(links) - - return ", ".join([user.username for user in asset.owners]) - - def render_groups_for_grid(self, asset, field, value): - asset_handler = self.app.get_asset_handler() - groups = asset_handler.get_groups(asset) - - if self.farmos_style_grid_links: - links = [] - for group in groups: - url = self.request.route_url( - f"{group.asset_type}_assets.view", uuid=group.uuid - ) - links.append(tags.link_to(str(group), url)) - return ", ".join(links) - - return ", ".join([str(group) for group in groups]) - - def render_locations_for_grid(self, asset, field, value): - asset_handler = self.app.get_asset_handler() - locations = asset_handler.get_locations(asset) - - if self.farmos_style_grid_links: - links = [] - for loc in locations: - url = self.request.route_url( - f"{loc.asset_type}_assets.view", uuid=loc.uuid - ) - links.append(tags.link_to(str(loc), url)) - return ", ".join(links) - - return ", ".join([str(loc) for loc in locations]) - def grid_row_class(self, asset, data, i): """ """ if asset.archived: @@ -220,7 +218,6 @@ class AssetMasterView(WuttaFarmMasterView): def configure_form(self, form): f = form super().configure_form(f) - asset_handler = self.app.get_asset_handler() asset = form.model_instance # asset_type @@ -233,39 +230,12 @@ class AssetMasterView(WuttaFarmMasterView): ) f.set_readonly("asset_type") - # owners - if self.creating or self.editing: - f.remove("owners") # TODO: need to support this - else: - f.set_node("owners", OwnerRefs(self.request)) - # nb. must explicity declare value for non-standard field - f.set_default("owners", asset.owners) - - # locations - if self.creating or self.editing: - # nb. this is a calculated field - f.remove("locations") - else: - f.set_label("locations", "Current Location") - f.set_node("locations", AssetRefs(self.request)) - # nb. must explicity declare value for non-standard field - f.set_default("locations", asset_handler.get_locations(asset)) - - # groups - if self.creating or self.editing: - # nb. this is a calculated field - f.remove("groups") - else: - f.set_node("groups", AssetRefs(self.request)) - # nb. must explicity declare value for non-standard field - f.set_default("groups", asset_handler.get_groups(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]) + f.set_default("parents", [p.parent_uuid for p in asset.asset._parents]) # notes f.set_widget("notes", "notes") @@ -296,14 +266,11 @@ class AssetMasterView(WuttaFarmMasterView): asset = super().objectify(form) if self.creating: - asset.asset_type = self.get_asset_type() + model_class = self.get_model_class() + asset.asset_type = model_class.__wutta_hint__["farmos_asset_type"] return asset - def get_asset_type(self): - model_class = self.get_model_class() - return model_class.__wutta_hint__["farmos_asset_type"] - def get_farmos_url(self, asset): return self.app.get_farmos_url(f"/asset/{asset.drupal_id}") @@ -311,7 +278,7 @@ class AssetMasterView(WuttaFarmMasterView): buttons = super().get_xref_buttons(asset) if asset.farmos_uuid: - asset_type = self.get_asset_type() + asset_type = self.get_model_class().__wutta_hint__["farmos_asset_type"] route = f"farmos_{asset_type}_assets.view" url = self.request.route_url(route, uuid=asset.farmos_uuid) buttons.append( @@ -322,21 +289,6 @@ class AssetMasterView(WuttaFarmMasterView): return buttons - def get_version_joins(self): - """ - We override this to declare the relationship between the - view's data model (which is some type of asset table) and the - canonical ``Asset`` model, so the revision history views - include transactions which reference either version table. - - See also parent method, - :meth:`~wuttaweb:wuttaweb.views.master.MasterView.get_version_joins()` - """ - model = self.app.model - return super().get_version_joins() + [ - model.Asset, - ] - def get_row_grid_data(self, asset): model = self.app.model session = self.Session() @@ -349,12 +301,7 @@ class AssetMasterView(WuttaFarmMasterView): def configure_row_grid(self, grid): g = grid super().configure_row_grid(g) - enum = self.app.enum model = self.app.model - session = self.Session() - - # status - g.set_enum("status", enum.LOG_STATUS) # drupal_id g.set_label("drupal_id", "ID", column_only=True) @@ -371,62 +318,16 @@ class AssetMasterView(WuttaFarmMasterView): # log_type g.set_sorter("log_type", model.Log.log_type) g.set_filter("log_type", model.Log.log_type) - g.set_enum("log_type", get_log_type_enum(self.config, session=session)) def get_row_action_url_view(self, log, i): return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) -class AllAssetView(AssetMasterView): - """ - Master view for Assets - """ - - model_class = Asset - route_prefix = "assets" - url_prefix = "/assets" - - farmos_refurl_path = "/assets" - - viewable = False - creatable = False - editable = False - deletable = False - model_is_versioned = False - - grid_columns = [ - "thumbnail", - "drupal_id", - "asset_name", - "groups", - "asset_type", - "parents", - "owners", - "locations", - "archived", - ] - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # asset_type - g.set_enum("asset_type", get_asset_type_enum(self.config)) - - # view action links to final asset record - def asset_url(asset, i): - return self.request.route_url( - f"{asset.asset_type}_assets.view", uuid=asset.uuid - ) - - g.add_action("view", icon="eye", url=asset_url) - - def defaults(config, **kwargs): base = globals() - AllAssetView = kwargs.get("AllAssetView", base["AllAssetView"]) - AllAssetView.defaults(config) + AssetView = kwargs.get("AssetView", base["AssetView"]) + AssetView.defaults(config) def includeme(config): diff --git a/src/wuttafarm/web/views/auth.py b/src/wuttafarm/web/views/auth.py index 0ab893d..db757cc 100644 --- a/src/wuttafarm/web/views/auth.py +++ b/src/wuttafarm/web/views/auth.py @@ -55,10 +55,9 @@ class AuthView(base.AuthView): return None def get_farmos_oauth2_session(self): - farmos = self.app.get_farmos_handler() return OAuth2Session( - client_id=farmos.get_oauth2_client_id(), - scope=farmos.get_oauth2_scope(), + client_id="farm", + scope="farm_manager", redirect_uri=self.request.route_url("farmos_oauth_callback"), ) diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py index 674d76e..f15e92b 100644 --- a/src/wuttafarm/web/views/common.py +++ b/src/wuttafarm/web/views/common.py @@ -87,20 +87,10 @@ class CommonView(base.CommonView): "farmos_logs_medical.view", "farmos_logs_observation.list", "farmos_logs_observation.view", - "farmos_plant_assets.list", - "farmos_plant_assets.view", - "farmos_plant_types.list", - "farmos_plant_types.view", - "farmos_quantities_standard.list", - "farmos_quantities_standard.view", - "farmos_quantity_types.list", - "farmos_quantity_types.view", "farmos_structure_assets.list", "farmos_structure_assets.view", "farmos_structure_types.list", "farmos_structure_types.view", - "farmos_units.list", - "farmos_units.view", "farmos_users.list", "farmos_users.view", "group_assets.create", @@ -131,7 +121,6 @@ class CommonView(base.CommonView): "logs_observation.list", "logs_observation.view", "logs_observation.versions", - "quick.eggs", "structure_types.list", "structure_types.view", "structure_types.versions", diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py index c99cc5a..690e7ee 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, FarmOSAssetRefs +from wuttafarm.web.forms.schema import FarmOSRef class AnimalView(AssetMasterView): @@ -87,9 +87,9 @@ class AnimalView(AssetMasterView): "is_sterile", "notes", "asset_type_name", + "groups", "owners", "locations", - "groups", "archived", "thumbnail_url", "image_url", @@ -147,7 +147,7 @@ class AnimalView(AssetMasterView): def render_groups_for_grid(self, animal, field, value): groups = [] - for group in animal["groups"]: + for group in animal["group_objects"]: if self.farmos_style_grid_links: url = self.request.route_url( "farmos_group_assets.view", uuid=group["uuid"] @@ -209,7 +209,6 @@ class AnimalView(AssetMasterView): group = { "uuid": group["id"], "name": group["attributes"]["name"], - "asset_type": "group", } group_objects.append(group) group_names.append(group["name"]) @@ -219,7 +218,7 @@ class AnimalView(AssetMasterView): "animal_type": animal_type_object, "animal_type_uuid": animal_type_object["uuid"], "animal_type_name": animal_type_object["name"], - "groups": group_objects, + "group_objects": group_objects, "group_names": group_names, "birthdate": birthdate, "sex": animal["attributes"]["sex"] or colander.null, @@ -274,8 +273,6 @@ class AnimalView(AssetMasterView): # 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 11f744b..d1ae226 100644 --- a/src/wuttafarm/web/views/farmos/assets.py +++ b/src/wuttafarm/web/views/farmos/assets.py @@ -53,8 +53,8 @@ class AssetMasterView(FarmOSMasterView): labels = { "name": "Asset Name", "asset_type_name": "Asset Type", + "owners": "Owner", "locations": "Location", - "groups": "Group Membership", "thumbnail_url": "Thumbnail URL", "image_url": "Image URL", } @@ -104,7 +104,6 @@ class AssetMasterView(FarmOSMasterView): g.set_filter("name", StringFilter) # owners - g.set_label("owners", "Owner") g.set_renderer("owners", self.render_owners_for_grid) # locations @@ -240,7 +239,6 @@ class AssetMasterView(FarmOSMasterView): if self.creating or self.editing: f.remove("locations") else: - f.set_label("locations", "Current Location") f.set_node("locations", FarmOSLocationRefs(self.request)) # owners diff --git a/src/wuttafarm/web/views/farmos/logs.py b/src/wuttafarm/web/views/farmos/logs.py index cb7a87b..6e6dc36 100644 --- a/src/wuttafarm/web/views/farmos/logs.py +++ b/src/wuttafarm/web/views/farmos/logs.py @@ -23,7 +23,6 @@ View for farmOS Harvest Logs """ -import colander from webhelpers2.html import tags from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum @@ -60,7 +59,6 @@ class LogMasterView(FarmOSMasterView): labels = { "name": "Log Name", "log_type_name": "Log Type", - "locations": "Location", "quantities": "Quantity", } @@ -70,7 +68,6 @@ class LogMasterView(FarmOSMasterView): "timestamp", "name", "assets", - "locations", "quantities", "is_group_assignment", "owners", @@ -87,21 +84,17 @@ class LogMasterView(FarmOSMasterView): "name", "timestamp", "assets", - "groups", - "locations", "quantities", "notes", "status", "log_type_name", "owners", - "is_movement", - "is_group_assignment", "quick", "drupal_id", ] def get_farmos_api_includes(self): - return {"log_type", "quantity", "asset", "group", "location", "owner"} + return {"log_type", "quantity", "asset", "owner"} def get_grid_data(self, **kwargs): return ResourceData( @@ -146,12 +139,6 @@ class LogMasterView(FarmOSMasterView): # assets g.set_renderer("assets", self.render_assets_for_grid) - # groups - g.set_renderer("groups", self.render_assets_for_grid) - - # locations - g.set_renderer("locations", self.render_assets_for_grid) - # quantities g.set_renderer("quantities", self.render_quantities_for_grid) @@ -165,9 +152,6 @@ class LogMasterView(FarmOSMasterView): g.set_renderer("owners", self.render_owners_for_grid) def render_assets_for_grid(self, log, field, value): - if not value: - return "" - assets = [] for asset in value: if self.farmos_style_grid_links: @@ -226,21 +210,9 @@ class LogMasterView(FarmOSMasterView): # assets f.set_node("assets", FarmOSAssetRefs(self.request)) - # groups - f.set_node("groups", FarmOSAssetRefs(self.request)) - - # locations - f.set_node("locations", FarmOSAssetRefs(self.request)) - # quantities f.set_node("quantities", FarmOSQuantityRefs(self.request)) - # is_movement - f.set_node("is_movement", colander.Boolean()) - - # is_group_assignment - f.set_node("is_group_assignment", colander.Boolean()) - # notes f.set_node("notes", Notes()) diff --git a/src/wuttafarm/web/views/farmos/logs_harvest.py b/src/wuttafarm/web/views/farmos/logs_harvest.py index bfe7121..08b2629 100644 --- a/src/wuttafarm/web/views/farmos/logs_harvest.py +++ b/src/wuttafarm/web/views/farmos/logs_harvest.py @@ -48,6 +48,7 @@ class HarvestLogView(LogMasterView): "name", "assets", "quantities", + "is_group_assignment", "owners", ] diff --git a/src/wuttafarm/web/views/farmos/logs_medical.py b/src/wuttafarm/web/views/farmos/logs_medical.py index 2f6a606..95a88c5 100644 --- a/src/wuttafarm/web/views/farmos/logs_medical.py +++ b/src/wuttafarm/web/views/farmos/logs_medical.py @@ -24,7 +24,6 @@ View for farmOS Medical Logs """ from wuttafarm.web.views.farmos.logs import LogMasterView -from wuttafarm.web.grids import SimpleSorter, StringFilter class MedicalLogView(LogMasterView): @@ -42,35 +41,6 @@ class MedicalLogView(LogMasterView): farmos_log_type = "medical" farmos_refurl_path = "/logs/medical" - labels = { - "vet": "Veterinarian", - } - - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "name", - "assets", - "vet", - "owners", - ] - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - - # vet - g.set_sorter("vet", SimpleSorter("vet")) - g.set_filter("vet", StringFilter) - - def configure_form(self, form): - f = form - super().configure_form(f) - - # vet - f.fields.insert_after("timestamp", "vet") - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/farmos/logs_observation.py b/src/wuttafarm/web/views/farmos/logs_observation.py index 0193f93..ab27b5a 100644 --- a/src/wuttafarm/web/views/farmos/logs_observation.py +++ b/src/wuttafarm/web/views/farmos/logs_observation.py @@ -41,18 +41,6 @@ class ObservationLogView(LogMasterView): farmos_log_type = "observation" farmos_refurl_path = "/logs/observation" - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "name", - "assets", - "locations", - "groups", - "is_group_assignment", - "owners", - ] - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/farmos/master.py b/src/wuttafarm/web/views/farmos/master.py index 1e2ceab..742ce14 100644 --- a/src/wuttafarm/web/views/farmos/master.py +++ b/src/wuttafarm/web/views/farmos/master.py @@ -34,7 +34,7 @@ from wuttaweb.views import MasterView from wuttaweb.forms.schema import WuttaDateTime from wuttaweb.forms.widgets import WuttaDateTimeWidget -from wuttafarm.web.util import get_farmos_client_for_user, use_farmos_style_grid_links +from wuttafarm.web.util import save_farmos_oauth2_token, use_farmos_style_grid_links from wuttafarm.web.grids import ( ResourceData, StringFilter, @@ -70,12 +70,28 @@ class FarmOSMasterView(MasterView): def __init__(self, request, context=None): super().__init__(request, context=context) - self.farmos_client = get_farmos_client_for_user(self.request) + self.farmos_client = self.get_farmos_client() self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.normal = self.app.get_normalizer(self.farmos_client) self.raw_json = None self.farmos_style_grid_links = use_farmos_style_grid_links(self.config) + def get_farmos_client(self): + token = self.request.session.get("farmos.oauth2.token") + if not token: + raise self.forbidden() + + # nb. must give a *copy* of the token to farmOS client, since + # it will mutate it in-place and we don't want that to happen + # for our original copy in the user session. (otherwise the + # auto-refresh will not work correctly for subsequent calls.) + token = dict(token) + + def token_updater(token): + save_farmos_oauth2_token(self.request, token) + + return self.app.get_farmos_client(token=token, token_updater=token_updater) + def get_fallback_templates(self, template): """ """ templates = super().get_fallback_templates(template) diff --git a/src/wuttafarm/web/views/farmos/quantities.py b/src/wuttafarm/web/views/farmos/quantities.py index a388559..8aafeea 100644 --- a/src/wuttafarm/web/views/farmos/quantities.py +++ b/src/wuttafarm/web/views/farmos/quantities.py @@ -32,7 +32,6 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget from wuttafarm.web.views.farmos import FarmOSMasterView from wuttafarm.web.forms.schema import FarmOSUnitRef -from wuttafarm.web.grids import ResourceData class QuantityTypeView(FarmOSMasterView): @@ -131,15 +130,13 @@ class QuantityMasterView(FarmOSMasterView): farmos_quantity_type = None grid_columns = [ - "drupal_id", - "as_text", "measure", "value", - "unit", "label", + "changed", ] - sort_defaults = ("drupal_id", "desc") + sort_defaults = ("changed", "desc") form_fields = [ "measure", @@ -150,58 +147,20 @@ class QuantityMasterView(FarmOSMasterView): "changed", ] - def get_farmos_api_includes(self): - return {"units"} - - def get_grid_data(self, **kwargs): - return ResourceData( - self.config, - self.farmos_client, - f"quantity--{self.farmos_quantity_type}", - include=",".join(self.get_farmos_api_includes()), - normalizer=self.normalize_quantity, - ) + def get_grid_data(self, columns=None, session=None): + result = self.farmos_client.resource.get("quantity", self.farmos_quantity_type) + return [self.normalize_quantity(t) for t in result["data"]] def configure_grid(self, grid): g = grid super().configure_grid(g) - # drupal_id - g.set_label("drupal_id", "ID", column_only=True) - - # as_text - g.set_renderer("as_text", self.render_as_text_for_grid) - - # measure - g.set_renderer("measure", self.render_measure_for_grid) - # value - g.set_renderer("value", self.render_value_for_grid) - - # unit - g.set_renderer("unit", self.render_unit_for_grid) + g.set_link("value") # changed g.set_renderer("changed", "datetime") - def render_as_text_for_grid(self, qty, field, value): - measure = qty["measure"].capitalize() - value = qty["value"]["decimal"] - units = qty["unit"]["name"] if qty["unit"] else "??" - return f"( {measure} ) {value} {units}" - - def render_measure_for_grid(self, qty, field, value): - return qty["measure"].capitalize() - - def render_unit_for_grid(self, qty, field, value): - unit = qty[field] - if not unit: - return "" - return unit["name"] - - def render_value_for_grid(self, qty, field, value): - return qty["value"]["decimal"] - def get_instance(self): quantity = self.farmos_client.resource.get_id( "quantity", self.farmos_quantity_type, self.request.matchdict["uuid"] @@ -228,7 +187,7 @@ class QuantityMasterView(FarmOSMasterView): def get_instance_title(self, quantity): return quantity["value"] - def normalize_quantity(self, quantity, included={}): + def normalize_quantity(self, quantity): if created := quantity["attributes"]["created"]: created = datetime.datetime.fromisoformat(created) @@ -238,37 +197,11 @@ class QuantityMasterView(FarmOSMasterView): changed = datetime.datetime.fromisoformat(changed) changed = self.app.localtime(changed) - quantity_type_object = None - quantity_type_uuid = None - unit_object = None - unit_uuid = None - if relationships := quantity["relationships"]: - - if quantity_type := relationships["quantity_type"]["data"]: - quantity_type_uuid = quantity_type["id"] - quantity_type_object = { - "uuid": quantity_type_uuid, - "type": "quantity_type--quantity_type", - } - - if unit := relationships["units"]["data"]: - unit_uuid = unit["id"] - if unit := included.get(unit_uuid): - unit_object = { - "uuid": unit_uuid, - "type": "taxonomy_term--unit", - "name": unit["attributes"]["name"], - } - return { "uuid": quantity["id"], "drupal_id": quantity["attributes"]["drupal_internal__id"], - "quantity_type": quantity_type_object, - "quantity_type_uuid": quantity_type_uuid, "measure": quantity["attributes"]["measure"], "value": quantity["attributes"]["value"], - "unit": unit_object, - "unit_uuid": unit_uuid, "label": quantity["attributes"]["label"] or colander.null, "created": created, "changed": changed, diff --git a/src/wuttafarm/web/views/groups.py b/src/wuttafarm/web/views/groups.py index 4331280..4b26463 100644 --- a/src/wuttafarm/web/views/groups.py +++ b/src/wuttafarm/web/views/groups.py @@ -37,7 +37,6 @@ class GroupView(AssetMasterView): url_prefix = "/assets/group" farmos_refurl_path = "/assets/group" - farmos_bundle = "group" grid_columns = [ "thumbnail", @@ -53,8 +52,8 @@ class GroupView(AssetMasterView): "asset_type", "produces_eggs", "archived", - "drupal_id", "farmos_uuid", + "drupal_id", ] diff --git a/src/wuttafarm/web/views/land.py b/src/wuttafarm/web/views/land.py index ca1f016..22827a0 100644 --- a/src/wuttafarm/web/views/land.py +++ b/src/wuttafarm/web/views/land.py @@ -51,8 +51,8 @@ class LandTypeView(AssetTypeMasterView): form_fields = [ "name", - "drupal_id", "farmos_uuid", + "drupal_id", ] has_rows = True @@ -129,19 +129,6 @@ class LandTypeView(AssetTypeMasterView): def get_row_action_url_view(self, land_asset, i): return self.request.route_url("land_assets.view", uuid=land_asset.uuid) - @classmethod - def defaults(cls, config): - """ """ - wutta_config = config.registry.settings.get("wutta_config") - app = wutta_config.get_app() - - if app.is_farmos_mirror(): - cls.creatable = False - cls.editable = False - cls.deletable = False - - cls._defaults(config) - class LandAssetView(AssetMasterView): """ @@ -152,7 +139,6 @@ class LandAssetView(AssetMasterView): route_prefix = "land_assets" url_prefix = "/assets/land" - farmos_bundle = "land" farmos_refurl_path = "/assets/land" grid_columns = [ @@ -173,8 +159,8 @@ class LandAssetView(AssetMasterView): "is_location", "is_fixed", "archived", - "drupal_id", "farmos_uuid", + "drupal_id", ] def configure_grid(self, grid): diff --git a/src/wuttafarm/web/views/logs.py b/src/wuttafarm/web/views/logs.py index 9c983b7..eeef49e 100644 --- a/src/wuttafarm/web/views/logs.py +++ b/src/wuttafarm/web/views/logs.py @@ -26,7 +26,6 @@ Base views for Logs from collections import OrderedDict import colander -from webhelpers2.html import tags, HTML from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.db import Session @@ -34,8 +33,18 @@ 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, LogQuantityRefs, OwnerRefs -from wuttafarm.util import get_log_type_enum +from wuttafarm.web.forms.schema import LogAssetRefs + + +def get_log_type_enum(config): + app = config.get_app() + model = app.model + session = Session() + log_types = OrderedDict() + query = session.query(model.LogType).order_by(model.LogType.name) + for log_type in query: + log_types[log_type.drupal_id] = log_type.name + return log_types class LogTypeView(WuttaFarmMasterView): @@ -61,8 +70,8 @@ class LogTypeView(WuttaFarmMasterView): form_fields = [ "name", "description", - "drupal_id", "farmos_uuid", + "drupal_id", ] def configure_grid(self, grid): @@ -90,17 +99,85 @@ class LogTypeView(WuttaFarmMasterView): return buttons +class LogView(WuttaFarmMasterView): + """ + Master view for All Logs + """ + + model_class = Log + route_prefix = "log" + url_prefix = "/logs" + + farmos_refurl_path = "/logs" + + viewable = False + creatable = False + editable = False + deletable = False + model_is_versioned = False + + labels = { + "message": "Log Name", + } + + grid_columns = [ + "status", + "drupal_id", + "timestamp", + "message", + "log_type", + "assets", + "location", + "quantity", + "groups", + "is_group_assignment", + ] + + sort_defaults = ("timestamp", "desc") + + filter_defaults = { + "message": {"active": True, "verb": "contains"}, + } + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # drupal_id + g.set_label("drupal_id", "ID", column_only=True) + + # timestamp + g.set_renderer("timestamp", "date") + g.set_link("timestamp") + + # message + g.set_link("message") + + # log_type + g.set_enum("log_type", get_log_type_enum(self.config)) + + # assets + g.set_renderer("assets", self.render_assets_for_grid) + + # view action links to final log record + def log_url(log, i): + return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) + + g.add_action("view", icon="eye", url=log_url) + + def render_assets_for_grid(self, log, field, value): + assets = [str(a.asset) for a in log._assets] + return ", ".join(assets) + + class LogMasterView(WuttaFarmMasterView): """ Base class for Asset master views """ - farmos_entity_type = "log" - labels = { "message": "Log Name", - "locations": "Location", - "quantities": "Quantity", + "owners": "Owner", } grid_columns = [ @@ -109,8 +186,8 @@ class LogMasterView(WuttaFarmMasterView): "timestamp", "message", "assets", - "locations", - "quantities", + # "location", + "quantity", "is_group_assignment", "owners", ] @@ -119,25 +196,21 @@ class LogMasterView(WuttaFarmMasterView): filter_defaults = { "message": {"active": True, "verb": "contains"}, - "status": {"active": True, "verb": "not_equal", "value": "abandoned"}, } form_fields = [ "message", "timestamp", "assets", - "groups", - "locations", - "quantities", + "location", + "quantity", "notes", "status", "log_type", "owners", - "is_movement", "is_group_assignment", - "quick", - "drupal_id", "farmos_uuid", + "drupal_id", ] def get_query(self, session=None): @@ -145,26 +218,16 @@ class LogMasterView(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.Log: - query = query.join(model.Log) - return query + return session.query(model_class).join(model.Log) def configure_grid(self, grid): g = grid super().configure_grid(g) model = self.app.model - enum = self.app.enum # status - g.set_enum("status", enum.LOG_STATUS) g.set_sorter("status", model.Log.status) - g.set_filter( - "status", - model.Log.status, - verbs=["equal", "not_equal"], - choices=enum.LOG_STATUS, - ) + g.set_filter("status", model.Log.status) # drupal_id g.set_label("drupal_id", "ID", column_only=True) @@ -185,68 +248,13 @@ class LogMasterView(WuttaFarmMasterView): # assets g.set_renderer("assets", self.render_assets_for_grid) - # groups - g.set_renderer("groups", self.render_assets_for_grid) - - # locations - g.set_renderer("locations", self.render_assets_for_grid) - - # quantities - g.set_renderer("quantities", self.render_quantities_for_grid) - - # is_group_assignment - g.set_renderer("is_group_assignment", "boolean") - g.set_sorter("is_group_assignment", model.Log.is_group_assignment) - g.set_filter("is_group_assignment", model.Log.is_group_assignment) - - # owners - g.set_label("owners", "Owner") - g.set_renderer("owners", self.render_owners_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 render_quantities_for_grid(self, log, field, value): - quantities = getattr(log, field) or [] - items = [] - for qty in quantities: - items.append(HTML.tag("li", c=qty.render_as_text(self.config))) - return HTML.tag("ul", c=items) - - def render_owners_for_grid(self, log, field, value): - - if self.farmos_style_grid_links: - links = [] - for user in log.owners: - url = self.request.route_url("users.view", uuid=user.uuid) - links.append(tags.link_to(user.username, url)) - return ", ".join(links) - - return ", ".join([user.username for user in log.owners]) - - def grid_row_class(self, log, data, i): - if log.status == "pending": - return "has-background-warning" - if log.status == "abandoned": - return "has-background-danger" - return None + return ", ".join([a.asset.asset_name for a in log.log._assets]) def configure_form(self, form): f = form super().configure_form(f) enum = self.app.enum - session = self.Session() log = f.model_instance # timestamp @@ -259,25 +267,12 @@ class LogMasterView(WuttaFarmMasterView): 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) + f.set_node("assets", LogAssetRefs(self.request)) + f.set_default("assets", [a.asset_uuid for a in log.log._assets]) - # groups + # location 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 - 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) + f.remove("location") # TODO: need to support this # log_type if self.creating: @@ -285,19 +280,13 @@ class LogMasterView(WuttaFarmMasterView): else: f.set_node( "log_type", - WuttaDictEnum( - self.request, get_log_type_enum(self.config, session=session) - ), + WuttaDictEnum(self.request, get_log_type_enum(self.config)), ) f.set_readonly("log_type") - # quantities + # quantity 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", log.quantities) + f.remove("quantity") # TODO: need to support this # notes f.set_widget("notes", "notes") @@ -305,23 +294,13 @@ class LogMasterView(WuttaFarmMasterView): # owners if self.creating or self.editing: f.remove("owners") # TODO: need to support this - else: - f.set_node("owners", OwnerRefs(self.request)) - # nb. must explicity declare value for non-standard field - f.set_default("owners", log.owners) # status f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS)) - # is_movement - f.set_node("is_movement", colander.Boolean()) - # is_group_assignment f.set_node("is_group_assignment", colander.Boolean()) - # quick - f.set_readonly("quick") # TODO - def objectify(self, form): log = super().objectify(form) @@ -354,68 +333,6 @@ class LogMasterView(WuttaFarmMasterView): return buttons - def get_version_joins(self): - """ - We override this to declare the relationship between the - view's data model (which is some type of log table) and the - canonical ``Log`` model, so the revision history views include - transactions which reference either version table. - - See also parent method, - :meth:`~wuttaweb:wuttaweb.views.master.MasterView.get_version_joins()` - """ - model = self.app.model - return super().get_version_joins() + [ - model.Log, - (model.LogAsset, "log_uuid", "uuid"), - ] - - -class AllLogView(LogMasterView): - """ - Master view for All Logs - """ - - model_class = Log - route_prefix = "log" - url_prefix = "/logs" - - farmos_refurl_path = "/logs" - - viewable = False - creatable = False - editable = False - deletable = False - model_is_versioned = False - - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "message", - "log_type", - "assets", - "locations", - "quantities", - "groups", - "is_group_assignment", - "owners", - ] - - def configure_grid(self, grid): - g = grid - super().configure_grid(g) - session = self.Session() - - # log_type - g.set_enum("log_type", get_log_type_enum(self.config, session=session)) - - # view action links to final log record - def log_url(log, i): - return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) - - g.add_action("view", icon="eye", url=log_url) - def defaults(config, **kwargs): base = globals() @@ -423,8 +340,8 @@ def defaults(config, **kwargs): LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"]) LogTypeView.defaults(config) - AllLogView = kwargs.get("AllLogView", base["AllLogView"]) - AllLogView.defaults(config) + LogView = kwargs.get("LogView", base["LogView"]) + LogView.defaults(config) def includeme(config): diff --git a/src/wuttafarm/web/views/logs_activity.py b/src/wuttafarm/web/views/logs_activity.py index 19f8782..dda3ca7 100644 --- a/src/wuttafarm/web/views/logs_activity.py +++ b/src/wuttafarm/web/views/logs_activity.py @@ -36,7 +36,6 @@ class ActivityLogView(LogMasterView): route_prefix = "logs_activity" url_prefix = "/logs/activity" - farmos_bundle = "activity" farmos_refurl_path = "/logs/activity" diff --git a/src/wuttafarm/web/views/logs_harvest.py b/src/wuttafarm/web/views/logs_harvest.py index e38c6d7..825c864 100644 --- a/src/wuttafarm/web/views/logs_harvest.py +++ b/src/wuttafarm/web/views/logs_harvest.py @@ -36,19 +36,8 @@ class HarvestLogView(LogMasterView): route_prefix = "logs_harvest" url_prefix = "/logs/harvest" - farmos_bundle = "harvest" farmos_refurl_path = "/logs/harvest" - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "message", - "assets", - "quantities", - "owners", - ] - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/logs_medical.py b/src/wuttafarm/web/views/logs_medical.py index d00d647..d582db9 100644 --- a/src/wuttafarm/web/views/logs_medical.py +++ b/src/wuttafarm/web/views/logs_medical.py @@ -36,29 +36,8 @@ class MedicalLogView(LogMasterView): route_prefix = "logs_medical" url_prefix = "/logs/medical" - farmos_bundle = "medical" farmos_refurl_path = "/logs/medical" - labels = { - "vet": "Veterinarian", - } - - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "message", - "assets", - "vet", - "owners", - ] - - def configure_form(self, f): - super().configure_form(f) - - # vet - f.fields.insert_after("timestamp", "vet") - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/logs_observation.py b/src/wuttafarm/web/views/logs_observation.py index 6e283ae..a4b9e8e 100644 --- a/src/wuttafarm/web/views/logs_observation.py +++ b/src/wuttafarm/web/views/logs_observation.py @@ -36,21 +36,8 @@ class ObservationLogView(LogMasterView): route_prefix = "logs_observation" url_prefix = "/logs/observation" - farmos_bundle = "observation" farmos_refurl_path = "/logs/observation" - grid_columns = [ - "status", - "drupal_id", - "timestamp", - "message", - "assets", - "locations", - "groups", - "is_group_assignment", - "owners", - ] - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/master.py b/src/wuttafarm/web/views/master.py index 747cdc5..2250d1b 100644 --- a/src/wuttafarm/web/views/master.py +++ b/src/wuttafarm/web/views/master.py @@ -27,7 +27,7 @@ from webhelpers2.html import tags from wuttaweb.views import MasterView -from wuttafarm.web.util import use_farmos_style_grid_links, get_farmos_client_for_user +from wuttafarm.web.util import use_farmos_style_grid_links class WuttaFarmMasterView(MasterView): @@ -36,8 +36,6 @@ class WuttaFarmMasterView(MasterView): """ farmos_refurl_path = None - farmos_entity_type = None - farmos_bundle = None labels = { "farmos_uuid": "farmOS UUID", @@ -106,42 +104,5 @@ class WuttaFarmMasterView(MasterView): f.set_readonly("drupal_id") def persist(self, obj, session=None): - - # save per usual super().persist(obj, session) - - # maybe also sync change to farmOS - if self.app.is_farmos_mirror(): - client = get_farmos_client_for_user(self.request) - self.app.auto_sync_to_farmos(obj, client=client, require=False) - - def get_farmos_entity_type(self): - if self.farmos_entity_type: - return self.farmos_entity_type - raise NotImplementedError( - f"must define {self.__class__.__name__}.farmos_entity_type" - ) - - def get_farmos_bundle(self): - if self.farmos_bundle: - return self.farmos_bundle - raise NotImplementedError( - f"must define {self.__class__.__name__}.farmos_bundle" - ) - - def delete_instance(self, obj): - - # save farmOS UUID if we need it - farmos_uuid = None - if hasattr(obj, "farmos_uuid") and self.app.is_farmos_mirror(): - farmos_uuid = obj.farmos_uuid - - # delete per usual - super().delete_instance(obj) - - # 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) - client.resource.delete(entity_type, bundle, farmos_uuid) + self.app.auto_sync_to_farmos(obj, require=False) diff --git a/src/wuttafarm/web/views/plants.py b/src/wuttafarm/web/views/plants.py index a114e07..4bd32c6 100644 --- a/src/wuttafarm/web/views/plants.py +++ b/src/wuttafarm/web/views/plants.py @@ -23,16 +23,12 @@ Master view for Plants """ -from webhelpers2.html import tags - from wuttaweb.forms.schema import WuttaDictEnum -from wuttaweb.util import get_form_data from wuttafarm.db.model import PlantType, PlantAsset from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView from wuttafarm.web.forms.schema import PlantTypeRefs from wuttafarm.web.forms.widgets import ImageWidget -from wuttafarm.web.util import get_farmos_client_for_user class PlantTypeView(AssetTypeMasterView): @@ -44,8 +40,6 @@ class PlantTypeView(AssetTypeMasterView): route_prefix = "plant_types" url_prefix = "/plant-types" - farmos_entity_type = "taxonomy_term" - farmos_bundle = "plant_type" farmos_refurl_path = "/admin/structure/taxonomy/manage/plant_type/overview" grid_columns = [ @@ -62,8 +56,8 @@ class PlantTypeView(AssetTypeMasterView): form_fields = [ "name", "description", - "drupal_id", "farmos_uuid", + "drupal_id", ] has_rows = True @@ -104,19 +98,6 @@ class PlantTypeView(AssetTypeMasterView): return buttons - def delete(self): - plant_type = self.get_instance() - - if plant_type._plant_assets: - self.request.session.flash( - "Cannot delete plant type which is still referenced by plant assets.", - "warning", - ) - url = self.get_action_url("view", plant_type) - return self.redirect(self.request.get_referrer(default=url)) - - return super().delete() - def get_row_grid_data(self, plant_type): model = self.app.model session = self.Session() @@ -145,55 +126,6 @@ class PlantTypeView(AssetTypeMasterView): 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 plant type. - """ - 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"} - - plant_type = model.PlantType(name=name) - session.add(plant_type) - session.flush() - - if self.app.is_farmos_mirror(): - client = get_farmos_client_for_user(self.request) - self.app.auto_sync_to_farmos(plant_type, client=client) - - return { - "uuid": plant_type.uuid.hex, - "name": plant_type.name, - "farmos_uuid": plant_type.farmos_uuid.hex, - "drupal_id": plant_type.drupal_id, - } - - @classmethod - def defaults(cls, config): - """ """ - cls._defaults(config) - cls._plant_type_defaults(config) - - @classmethod - def _plant_type_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): """ @@ -204,7 +136,6 @@ class PlantAssetView(AssetMasterView): route_prefix = "plant_assets" url_prefix = "/assets/plant" - farmos_bundle = "plant" farmos_refurl_path = "/assets/plant" labels = { @@ -227,8 +158,8 @@ class PlantAssetView(AssetMasterView): "notes", "asset_type", "archived", - "drupal_id", "farmos_uuid", + "drupal_id", "thumbnail_url", "image_url", "thumbnail", @@ -240,20 +171,10 @@ class PlantAssetView(AssetMasterView): super().configure_grid(g) # plant_types - g.set_renderer("plant_types", self.render_plant_types_for_grid) + g.set_renderer("plant_types", self.render_grid_plant_types) - def render_plant_types_for_grid(self, plant, field, value): - plant_types = plant._plant_types - - if self.farmos_style_grid_links: - links = [] - for plant_type in plant_types: - plant_type = plant_type.plant_type - url = self.request.route_url("plant_types.view", uuid=plant_type.uuid) - links.append(tags.link_to(str(plant_type), url)) - return ", ".join(links) - - return ", ".join([str(pt.plant_type) for pt in plant_types]) + def render_grid_plant_types(self, plant, field, value): + return ", ".join([t.plant_type.name for t in plant._plant_types]) def configure_form(self, form): f = form @@ -262,38 +183,18 @@ class PlantAssetView(AssetMasterView): plant = f.model_instance # 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]) + if self.creating or self.editing: + f.remove("plant_types") # TODO: add support for this + else: + f.set_node("plant_types", PlantTypeRefs(self.request)) + f.set_default( + "plant_types", [t.plant_type_uuid for t in plant._plant_types] + ) # season 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 - - current = [pt.uuid for pt in plant.plant_types] - desired = data["plant_types"] - - for uuid in desired: - if uuid not in current: - plant_type = session.get(model.PlantType, uuid) - assert plant_type - plant.plant_types.append(plant_type) - - for uuid in current: - if uuid not in desired: - plant_type = session.get(model.PlantType, uuid) - assert plant_type - plant.plant_types.remove(plant_type) - - return plant - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/quantities.py b/src/wuttafarm/web/views/quantities.py index d4112cf..7d75290 100644 --- a/src/wuttafarm/web/views/quantities.py +++ b/src/wuttafarm/web/views/quantities.py @@ -29,7 +29,7 @@ from wuttaweb.db import Session from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity -from wuttafarm.web.forms.schema import UnitRef, LogRef +from wuttafarm.web.forms.schema import UnitRef def get_quantity_type_enum(config): @@ -66,8 +66,8 @@ class QuantityTypeView(WuttaFarmMasterView): form_fields = [ "name", "description", - "drupal_id", "farmos_uuid", + "drupal_id", ] def configure_grid(self, grid): @@ -119,9 +119,8 @@ class QuantityMasterView(WuttaFarmMasterView): "value", "units", "label", - "log", - "drupal_id", "farmos_uuid", + "drupal_id", ] def get_query(self, session=None): @@ -232,13 +231,6 @@ class QuantityMasterView(WuttaFarmMasterView): # TODO: ugh f.set_default("units", quantity.quantity.units) - # log - if self.creating or self.editing: - f.remove("log") - else: - f.set_node("log", LogRef(self.request)) - f.set_default("log", quantity.log) - def get_xref_buttons(self, quantity): buttons = super().get_xref_buttons(quantity) @@ -256,7 +248,7 @@ class QuantityMasterView(WuttaFarmMasterView): return buttons -class AllQuantityView(QuantityMasterView): +class QuantityView(QuantityMasterView): """ Master view for All Quantities """ @@ -288,8 +280,8 @@ def defaults(config, **kwargs): QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"]) QuantityTypeView.defaults(config) - AllQuantityView = kwargs.get("AllQuantityView", base["AllQuantityView"]) - AllQuantityView.defaults(config) + QuantityView = kwargs.get("QuantityView", base["QuantityView"]) + QuantityView.defaults(config) StandardQuantityView = kwargs.get( "StandardQuantityView", base["StandardQuantityView"] diff --git a/src/wuttafarm/web/views/quick/__init__.py b/src/wuttafarm/web/views/quick/__init__.py index 8423b0d..92595e1 100644 --- a/src/wuttafarm/web/views/quick/__init__.py +++ b/src/wuttafarm/web/views/quick/__init__.py @@ -27,9 +27,4 @@ from .base import QuickFormView def includeme(config): - - # perm group - config.add_wutta_permission_group("quick", "Quick Forms", overwrite=False) - - # quick form views config.include("wuttafarm.web.views.quick.eggs") diff --git a/src/wuttafarm/web/views/quick/base.py b/src/wuttafarm/web/views/quick/base.py index 059ac01..2fb73e4 100644 --- a/src/wuttafarm/web/views/quick/base.py +++ b/src/wuttafarm/web/views/quick/base.py @@ -28,9 +28,8 @@ import logging from pyramid.renderers import render_to_response from wuttaweb.views import View -from wuttaweb.db import Session -from wuttafarm.web.util import get_farmos_client_for_user +from wuttafarm.web.util import save_farmos_oauth2_token log = logging.getLogger(__name__) @@ -41,11 +40,9 @@ class QuickFormView(View): Base class for quick form views. """ - Session = Session - def __init__(self, request, context=None): super().__init__(request, context=context) - self.farmos_client = get_farmos_client_for_user(self.request) + self.farmos_client = self.get_farmos_client() self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.normal = self.app.get_normalizer(self.farmos_client) @@ -130,6 +127,22 @@ class QuickFormView(View): def get_template_context(self, context): return context + def get_farmos_client(self): + token = self.request.session.get("farmos.oauth2.token") + if not token: + raise self.forbidden() + + # nb. must give a *copy* of the token to farmOS client, since + # it will mutate it in-place and we don't want that to happen + # for our original copy in the user session. (otherwise the + # auto-refresh will not work correctly for subsequent calls.) + token = dict(token) + + def token_updater(token): + save_farmos_oauth2_token(self.request, token) + + return self.app.get_farmos_client(token=token, token_updater=token_updater) + @classmethod def defaults(cls, config): cls._defaults(config) @@ -138,10 +151,6 @@ class QuickFormView(View): def _defaults(cls, config): route_slug = cls.get_route_slug() url_slug = cls.get_url_slug() - form_title = cls.get_form_title() - config.add_wutta_permission("quick", f"quick.{route_slug}", form_title) config.add_route(f"quick.{route_slug}", f"/quick/{url_slug}") - config.add_view( - cls, route_name=f"quick.{route_slug}", permission=f"quick.{route_slug}" - ) + config.add_view(cls, route_name=f"quick.{route_slug}") diff --git a/src/wuttafarm/web/views/quick/eggs.py b/src/wuttafarm/web/views/quick/eggs.py index 8aae46e..aa663b6 100644 --- a/src/wuttafarm/web/views/quick/eggs.py +++ b/src/wuttafarm/web/views/quick/eggs.py @@ -48,9 +48,6 @@ class EggsQuickForm(QuickFormView): _layer_assets = None - # TODO: make this configurable? - unit_name = "egg(s)" - def make_quick_form(self): f = self.make_form( fields=[ @@ -91,47 +88,6 @@ class EggsQuickForm(QuickFormView): if self._layer_assets is not None: return self._layer_assets - if self.app.is_farmos_wrapper(): - assets = self.get_layer_assets_from_farmos() - else: - assets = self.get_layer_assets_from_wuttafarm() - - assets.sort(key=lambda a: a["name"]) - self._layer_assets = assets - return assets - - def get_layer_assets_from_wuttafarm(self): - model = self.app.model - session = self.Session() - assets = [] - - def normalize(asset): - asset_type = asset.__wutta_hint__["farmos_asset_type"] - return { - "uuid": str(asset.farmos_uuid), - "name": asset.asset_name, - "type": f"asset--{asset_type}", - } - - query = ( - session.query(model.AnimalAsset) - .join(model.Asset) - .filter(model.AnimalAsset.produces_eggs == True) - .order_by(model.Asset.asset_name) - ) - assets.extend([normalize(a) for a in query]) - - query = ( - session.query(model.GroupAsset) - .join(model.Asset) - .filter(model.GroupAsset.produces_eggs == True) - .order_by(model.Asset.asset_name) - ) - assets.extend([normalize(a) for a in query]) - - return assets - - def get_layer_assets_from_farmos(self): assets = [] params = { "filter[produces_eggs]": 1, @@ -151,14 +107,21 @@ class EggsQuickForm(QuickFormView): result = self.farmos_client.asset.get("group", params=params) assets.extend([normalize(a) for a in result["data"]]) + assets.sort(key=lambda a: a["name"]) + self._layer_assets = assets return assets def save_quick_form(self, form): - if self.app.is_farmos_wrapper(): - return self.save_to_farmos(form) + response = self.save_to_farmos(form) + log = json.loads(response["create-log#body{0}"]["body"]) - return self.save_to_wuttafarm(form) + if self.app.is_farmos_mirror(): + quantity = json.loads(response["create-quantity"]["body"]) + self.app.auto_sync_from_farmos(quantity["data"], "StandardQuantity") + self.app.auto_sync_from_farmos(log["data"], "HarvestLog") + + return log def save_to_farmos(self, form): data = form.validated @@ -168,7 +131,7 @@ class EggsQuickForm(QuickFormView): asset = assets[data["asset"]] # TODO: make this configurable? - unit_name = self.unit_name + unit_name = "egg(s)" unit = {"data": {"type": "taxonomy_term--unit"}} new_unit = None @@ -229,7 +192,6 @@ class EggsQuickForm(QuickFormView): "type": "log--harvest", "attributes": { "name": f"Collected {data['count']} {unit_name}", - "timestamp": self.app.localtime(data["timestamp"]).timestamp(), "notes": notes, "quick": ["eggs"], }, @@ -267,87 +229,13 @@ class EggsQuickForm(QuickFormView): blueprints.insert(0, new_unit) blueprint = SubrequestsBlueprint.parse_obj(blueprints) response = self.farmos_client.subrequests.send(blueprint, format=Format.json) + return response - log = json.loads(response["create-log#body{0}"]["body"]) - - if self.app.is_farmos_mirror(): - if new_unit: - unit = json.loads(response["create-unit"]["body"]) - self.app.auto_sync_from_farmos( - unit["data"], "Unit", client=self.farmos_client - ) - quantity = json.loads(response["create-quantity"]["body"]) - self.app.auto_sync_from_farmos( - quantity["data"], "StandardQuantity", client=self.farmos_client - ) - self.app.auto_sync_from_farmos( - log["data"], "HarvestLog", client=self.farmos_client - ) - - return log - - def save_to_wuttafarm(self, form): - model = self.app.model - session = self.Session() - data = form.validated - - asset = ( - session.query(model.Asset) - .filter(model.Asset.farmos_uuid == data["asset"]) - .one() - ) - - # TODO: make this configurable? - unit_name = self.unit_name - - new_unit = False - unit = session.query(model.Unit).filter(model.Unit.name == unit_name).first() - if not unit: - unit = model.Unit(name=unit_name) - session.add(unit) - new_unit = True - - quantity = model.StandardQuantity( - quantity_type_id="standard", - measure_id="count", - value_numerator=data["count"], - value_denominator=1, - units=unit, - ) - session.add(quantity) - - log = model.HarvestLog( - log_type="harvest", - message=f"Collected {data['count']} {unit_name}", - timestamp=self.app.make_utc(data["timestamp"]), - notes=data["notes"] or None, - quick="eggs", - status="done", - ) - session.add(log) - log.assets.append(asset) - log.quantities.append(quantity.quantity) - log.owners.append(self.request.user) - session.flush() - - if self.app.is_farmos_mirror(): - if new_unit: - self.app.auto_sync_to_farmos(unit, client=self.farmos_client) - self.app.auto_sync_to_farmos(quantity, client=self.farmos_client) - self.app.auto_sync_to_farmos(log, client=self.farmos_client) - - return log - - def redirect_after_save(self, log): - model = self.app.model - - if isinstance(log, model.HarvestLog): - return self.redirect( - self.request.route_url("logs_harvest.view", uuid=log.uuid) - ) - + def redirect_after_save(self, result): return self.redirect( - self.request.route_url("farmos_logs_harvest.view", uuid=log["data"]["id"]) + self.request.route_url( + "farmos_logs_harvest.view", uuid=result["data"]["id"] + ) ) diff --git a/src/wuttafarm/web/views/settings.py b/src/wuttafarm/web/views/settings.py index 74cc0a0..86d7a0c 100644 --- a/src/wuttafarm/web/views/settings.py +++ b/src/wuttafarm/web/views/settings.py @@ -57,21 +57,10 @@ class AppInfoView(base.AppInfoView): return info def configure_get_simple_settings(self): # pylint: disable=empty-docstring - farmos = self.app.get_farmos_handler() simple_settings = super().configure_get_simple_settings() simple_settings.extend( [ - { - "name": "farmos.url.base", - }, - { - "name": "farmos.oauth2.client_id", - "default": farmos.get_oauth2_client_id(), - }, - { - "name": "farmos.oauth2.scope", - "default": farmos.get_oauth2_scope(), - }, + {"name": "farmos.url.base"}, { "name": f"{self.app.appname}.farmos_integration_mode", "default": self.app.get_farmos_integration_mode(), diff --git a/src/wuttafarm/web/views/structures.py b/src/wuttafarm/web/views/structures.py index e17a39f..aa9bf31 100644 --- a/src/wuttafarm/web/views/structures.py +++ b/src/wuttafarm/web/views/structures.py @@ -50,8 +50,8 @@ class StructureTypeView(AssetTypeMasterView): form_fields = [ "name", - "drupal_id", "farmos_uuid", + "drupal_id", ] has_rows = True @@ -128,19 +128,6 @@ class StructureTypeView(AssetTypeMasterView): def get_row_action_url_view(self, structure, i): return self.request.route_url("structure_assets.view", uuid=structure.uuid) - @classmethod - def defaults(cls, config): - """ """ - wutta_config = config.registry.settings.get("wutta_config") - app = wutta_config.get_app() - - if app.is_farmos_mirror(): - cls.creatable = False - cls.editable = False - cls.deletable = False - - cls._defaults(config) - class StructureAssetView(AssetMasterView): """ @@ -151,7 +138,6 @@ class StructureAssetView(AssetMasterView): route_prefix = "structure_assets" url_prefix = "/asset/structures" - farmos_bundle = "structure" farmos_refurl_path = "/assets/structure" grid_columns = [ @@ -160,7 +146,6 @@ class StructureAssetView(AssetMasterView): "asset_name", "structure_type", "parents", - "owners", "archived", ] @@ -173,8 +158,8 @@ class StructureAssetView(AssetMasterView): "is_location", "is_fixed", "archived", - "drupal_id", "farmos_uuid", + "drupal_id", "thumbnail_url", "image_url", "thumbnail", diff --git a/src/wuttafarm/web/views/units.py b/src/wuttafarm/web/views/units.py index fe8dafe..3b86426 100644 --- a/src/wuttafarm/web/views/units.py +++ b/src/wuttafarm/web/views/units.py @@ -24,7 +24,7 @@ Master view for Units """ from wuttafarm.web.views import WuttaFarmMasterView -from wuttafarm.db.model import Measure, Unit, Quantity +from wuttafarm.db.model import Measure, Unit class MeasureView(WuttaFarmMasterView): @@ -52,26 +52,6 @@ class MeasureView(WuttaFarmMasterView): "drupal_id", ] - has_rows = True - row_model_class = Quantity - rows_viewable = True - - row_labels = { - "quantity_type_id": "Quantity Type ID", - "measure_id": "Measure ID", - } - - row_grid_columns = [ - "drupal_id", - "as_text", - "quantity_type", - "value", - "units", - "label", - ] - - rows_sort_defaults = ("drupal_id", "desc") - def configure_grid(self, grid): g = grid super().configure_grid(g) @@ -79,50 +59,6 @@ class MeasureView(WuttaFarmMasterView): # name g.set_link("name") - def get_row_grid_data(self, measure): - model = self.app.model - session = self.Session() - return session.query(model.Quantity).filter(model.Quantity.measure == measure) - - def configure_row_grid(self, grid): - g = grid - super().configure_row_grid(g) - - # drupal_id - g.set_label("drupal_id", "ID", column_only=True) - - # as_text - g.set_renderer("as_text", self.render_as_text_for_grid) - g.set_link("as_text") - - # value - g.set_renderer("value", self.render_value_for_grid) - - def render_as_text_for_grid(self, quantity, field, value): - return quantity.render_as_text(self.config) - - def render_value_for_grid(self, quantity, field, value): - value = quantity.value_numerator / quantity.value_denominator - return self.app.render_quantity(value) - - def get_row_action_url_view(self, quantity, i): - return self.request.route_url( - f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid - ) - - @classmethod - def defaults(cls, config): - """ """ - wutta_config = config.registry.settings.get("wutta_config") - app = wutta_config.get_app() - - if app.is_farmos_mirror(): - cls.creatable = False - cls.editable = False - cls.deletable = False - - cls._defaults(config) - class UnitView(WuttaFarmMasterView): """ @@ -133,8 +69,6 @@ class UnitView(WuttaFarmMasterView): route_prefix = "units" url_prefix = "/units" - farmos_entity_type = "taxonomy_term" - farmos_bundle = "unit" farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview" grid_columns = [ @@ -151,30 +85,10 @@ class UnitView(WuttaFarmMasterView): form_fields = [ "name", "description", - "drupal_id", "farmos_uuid", - ] - - has_rows = True - row_model_class = Quantity - rows_viewable = True - - row_labels = { - "quantity_type_id": "Quantity Type ID", - "measure_id": "Measure ID", - } - - row_grid_columns = [ "drupal_id", - "as_text", - "quantity_type", - "measure", - "value", - "label", ] - rows_sort_defaults = ("drupal_id", "desc") - def configure_grid(self, grid): g = grid super().configure_grid(g) @@ -202,37 +116,6 @@ class UnitView(WuttaFarmMasterView): return buttons - def get_row_grid_data(self, unit): - model = self.app.model - session = self.Session() - return session.query(model.Quantity).filter(model.Quantity.units == unit) - - def configure_row_grid(self, grid): - g = grid - super().configure_row_grid(g) - - # drupal_id - g.set_label("drupal_id", "ID", column_only=True) - - # as_text - g.set_renderer("as_text", self.render_as_text_for_grid) - g.set_link("as_text") - - # value - g.set_renderer("value", self.render_value_for_grid) - - def render_as_text_for_grid(self, quantity, field, value): - return quantity.render_as_text(self.config) - - def render_value_for_grid(self, quantity, field, value): - value = quantity.value_numerator / quantity.value_denominator - return self.app.render_quantity(value) - - def get_row_action_url_view(self, quantity, i): - return self.request.route_url( - f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid - ) - def defaults(config, **kwargs): base = globals() diff --git a/src/wuttafarm/web/views/users.py b/src/wuttafarm/web/views/users.py index ffda747..21e26d9 100644 --- a/src/wuttafarm/web/views/users.py +++ b/src/wuttafarm/web/views/users.py @@ -55,13 +55,11 @@ class UserView(base.UserView): # farmos_uuid if not self.creating: f.fields.append("farmos_uuid") - f.set_readonly("farmos_uuid") f.set_default("farmos_uuid", user.farmos_uuid or colander.null) # drupal_id if not self.creating: f.fields.append("drupal_id") - f.set_readonly("drupal_id") f.set_default("drupal_id", user.drupal_id or colander.null) def get_xref_buttons(self, user):