Compare commits

..

No commits in common. "master" and "v0.6.0" have entirely different histories.

70 changed files with 518 additions and 3761 deletions

View file

@ -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/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.8.0 (2026-03-04)
### Feat
- improve support for exporting quantity, log data
- show related Quantity records when viewing a Measure
- show related Quantity records when viewing a Unit
- show link to Log record when viewing Quantity
### Fix
- bump version requirement for wuttaweb
## v0.7.0 (2026-03-04)
### 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) ## v0.6.0 (2026-02-25)
### Feat ### Feat

View file

@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "WuttaFarm" name = "WuttaFarm"
version = "0.8.0" version = "0.6.0"
description = "Web app to integrate with and extend farmOS" description = "Web app to integrate with and extend farmOS"
readme = "README.md" readme = "README.md"
authors = [ authors = [
@ -34,7 +34,7 @@ dependencies = [
"pyramid_exclog", "pyramid_exclog",
"uvicorn[standard]", "uvicorn[standard]",
"WuttaSync", "WuttaSync",
"WuttaWeb[continuum]>=0.29.0", "WuttaWeb[continuum]>=0.28.1",
] ]

View file

@ -36,21 +36,6 @@ class WuttaFarmAppHandler(base.AppHandler):
default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler" default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler" 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): def get_farmos_handler(self):
""" """
Get the configured farmOS integration handler. Get the configured farmOS integration handler.
@ -151,7 +136,7 @@ class WuttaFarmAppHandler(base.AppHandler):
factory = self.load_object(spec) factory = self.load_object(spec)
return factory(self.config, farmos_client) 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. 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 :param obj: Any data object in WuttaFarm, e.g. AnimalAsset
instance. 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 :param require: If true, this will *require* the export
handler to support objects of the given type. If false, handler to support objects of the given type. If false,
then nothing will happen / export is silently skipped when then nothing will happen / export is silently skipped when
@ -180,12 +162,14 @@ class WuttaFarmAppHandler(base.AppHandler):
return return
# nb. begin txn to establish the API client # 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) importer = handler.get_importer(model_name, caches_target=False)
normal = importer.normalize_source_object(obj) normal = importer.normalize_source_object(obj)
importer.process_data(source_data=[normal]) 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. 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, :param model_name': Model name for the importer to use,
e.g. ``"AnimalAsset"``. 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 :param require: If true, this will *require* the import
handler to support objects of the given type. If false, handler to support objects of the given type. If false,
then nothing will happen / import is silently skipped when then nothing will happen / import is silently skipped when
@ -210,7 +191,9 @@ class WuttaFarmAppHandler(base.AppHandler):
return return
# nb. begin txn to establish the API client # 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: with self.short_session(commit=True) as session:
handler.target_session = session handler.target_session = session
importer = handler.get_importer(model_name, caches_target=False) importer = handler.get_importer(model_name, caches_target=False)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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 []

View file

@ -50,12 +50,11 @@ class WuttaFarmConfig(WuttaConfigExtension):
f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler" f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler"
) )
# web app stuff # web app menu
config.setdefault( config.setdefault(
f"{config.appname}.web.menus.handler.default_spec", f"{config.appname}.web.menus.handler.default_spec",
"wuttafarm.web.menus:WuttaFarmMenuHandler", "wuttafarm.web.menus:WuttaFarmMenuHandler",
) )
config.setdefault("wuttaweb.grids.default_pagesize", "50")
# web app libcache # web app libcache
# config.setdefault('wuttaweb.static_libcache.module', 'wuttafarm.web.static') # config.setdefault('wuttaweb.static_libcache.module', 'wuttafarm.web.static')

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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,
)

View file

@ -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")

View file

@ -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,
)

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -38,7 +38,7 @@ from .asset_structure import StructureType, StructureAsset
from .asset_animal import AnimalType, AnimalAsset from .asset_animal import AnimalType, AnimalAsset
from .asset_group import GroupAsset from .asset_group import GroupAsset
from .asset_plant import PlantType, PlantAsset, PlantAssetPlantType from .asset_plant import PlantType, 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_activity import ActivityLog
from .log_harvest import HarvestLog from .log_harvest import HarvestLog
from .log_medical import MedicalLog from .log_medical import MedicalLog

View file

@ -26,7 +26,6 @@ Model definition for Asset Types
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db import model from wuttjamaican.db import model
@ -187,25 +186,6 @@ class Asset(model.Base):
cascade_backrefs=False, 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): def __str__(self):
return self.asset_name or "" return self.asset_name or ""
@ -216,12 +196,7 @@ class AssetMixin:
@declared_attr @declared_attr
def asset(cls): def asset(cls):
return orm.relationship( return orm.relationship(Asset)
Asset,
single_parent=True,
cascade="all, delete-orphan",
cascade_backrefs=False,
)
def __str__(self): def __str__(self):
return self.asset_name or "" 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", "thumbnail_url")
Asset.make_proxy(subclass, "asset", "image_url") Asset.make_proxy(subclass, "asset", "image_url")
Asset.make_proxy(subclass, "asset", "archived") Asset.make_proxy(subclass, "asset", "archived")
Asset.make_proxy(subclass, "asset", "parents")
Asset.make_proxy(subclass, "asset", "owners")
class EggMixin: class EggMixin:
@ -277,27 +250,3 @@ class AssetParent(model.Base):
Asset, Asset,
foreign_keys=parent_uuid, 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,
)

View file

@ -48,6 +48,7 @@ class AnimalType(model.Base):
name = sa.Column( name = sa.Column(
sa.String(length=100), sa.String(length=100),
nullable=False, nullable=False,
unique=True,
doc=""" doc="""
Name of the animal type. 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): def __str__(self):
return self.name or "" return self.name or ""
@ -110,7 +103,6 @@ class AnimalAsset(AssetMixin, EggMixin, model.Base):
doc=""" doc="""
Reference to the animal type. Reference to the animal type.
""", """,
back_populates="animal_assets",
) )
birthdate = sa.Column( birthdate = sa.Column(

View file

@ -88,10 +88,10 @@ class LandAsset(AssetMixin, model.Base):
__wutta_hint__ = { __wutta_hint__ = {
"model_title": "Land Asset", "model_title": "Land Asset",
"model_title_plural": "Land Assets", "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") land_type = orm.relationship(LandType, back_populates="land_assets")

View file

@ -25,7 +25,6 @@ Model definition for Plant Assets
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db import model 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): def __str__(self):
return self.name or "" return self.name or ""
@ -106,17 +99,9 @@ class PlantAsset(AssetMixin, model.Base):
_plant_types = orm.relationship( _plant_types = orm.relationship(
"PlantAssetPlantType", "PlantAssetPlantType",
cascade="all, delete-orphan",
cascade_backrefs=False,
back_populates="plant_asset", back_populates="plant_asset",
) )
plant_types = association_proxy(
"_plant_types",
"plant_type",
creator=lambda pt: PlantAssetPlantType(plant_type=pt),
)
add_asset_proxies(PlantAsset) add_asset_proxies(PlantAsset)
@ -144,5 +129,4 @@ class PlantAssetPlantType(model.Base):
doc=""" doc="""
Reference to the plant type. Reference to the plant type.
""", """,
back_populates="_plant_assets",
) )

View file

@ -26,7 +26,6 @@ Model definition for Logs
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db import model from wuttjamaican.db import model
@ -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( status = sa.Column(
sa.String(length=20), sa.String(length=20),
nullable=False, 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( farmos_uuid = sa.Column(
model.UUID(), model.UUID(),
nullable=True, nullable=True,
@ -179,70 +153,7 @@ class Log(model.Base):
""", """,
) )
_assets = orm.relationship( _assets = orm.relationship("LogAsset", back_populates="log")
"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),
)
def __str__(self): def __str__(self):
return self.message or "" return self.message or ""
@ -254,12 +165,7 @@ class LogMixin:
@declared_attr @declared_attr
def log(cls): def log(cls):
return orm.relationship( return orm.relationship(Log)
Log,
single_parent=True,
cascade="all, delete-orphan",
cascade_backrefs=False,
)
def __str__(self): def __str__(self):
return self.message or "" 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", "log_type")
Log.make_proxy(subclass, "log", "message") Log.make_proxy(subclass, "log", "message")
Log.make_proxy(subclass, "log", "timestamp") 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", "status")
Log.make_proxy(subclass, "log", "notes") 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): class LogAsset(model.Base):
@ -305,100 +203,3 @@ class LogAsset(model.Base):
"Asset", "Asset",
foreign_keys=asset_uuid, 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,
)

View file

@ -23,8 +23,6 @@
Model definition for Medical Logs Model definition for Medical Logs
""" """
import sqlalchemy as sa
from wuttjamaican.db import model from wuttjamaican.db import model
from wuttafarm.db.model.log import LogMixin, add_log_proxies from wuttafarm.db.model.log import LogMixin, add_log_proxies
@ -43,13 +41,5 @@ class MedicalLog(LogMixin, model.Base):
"farmos_log_type": "medical", "farmos_log_type": "medical",
} }
vet = sa.Column(
sa.String(length=100),
nullable=True,
doc="""
Name of the veterinarian, if applicable.
""",
)
add_log_proxies(MedicalLog) add_log_proxies(MedicalLog)

View file

@ -26,7 +26,6 @@ Model definition for Quantities
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db import model from wuttjamaican.db import model
@ -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): def render_as_text(self, config=None):
measure = str(self.measure or self.measure_id or "") measure = str(self.measure or self.measure_id or "")
value = self.value_numerator / self.value_denominator value = self.value_numerator / self.value_denominator
@ -222,7 +202,6 @@ def add_quantity_proxies(subclass):
Quantity.make_proxy(subclass, "quantity", "units_uuid") Quantity.make_proxy(subclass, "quantity", "units_uuid")
Quantity.make_proxy(subclass, "quantity", "units") Quantity.make_proxy(subclass, "quantity", "units")
Quantity.make_proxy(subclass, "quantity", "label") Quantity.make_proxy(subclass, "quantity", "label")
Quantity.make_proxy(subclass, "quantity", "log")
class StandardQuantity(QuantityMixin, model.Base): class StandardQuantity(QuantityMixin, model.Base):

View file

@ -94,9 +94,3 @@ class FarmOSHandler(GenericHandler):
return f"{base}/{path}" return f"{base}/{path}"
return base 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")

View file

@ -347,12 +347,6 @@ class LandAssetImporter(ToFarmOSAsset):
return payload return payload
class PlantTypeImporter(ToFarmOSTaxonomy):
model_title = "PlantType"
farmos_taxonomy_type = "plant_type"
class PlantAssetImporter(ToFarmOSAsset): class PlantAssetImporter(ToFarmOSAsset):
model_title = "PlantAsset" model_title = "PlantAsset"
@ -443,138 +437,6 @@ class StructureAssetImporter(ToFarmOSAsset):
return payload return payload
##############################
# quantity importers
##############################
class ToFarmOSQuantity(ToFarmOS):
"""
Base class for quantity data importer targeting the farmOS API.
"""
farmos_quantity_type = None
supported_fields = [
"uuid",
"measure",
"value_numerator",
"value_denominator",
"label",
"quantity_type_uuid",
"unit_uuid",
]
def get_target_objects(self, **kwargs):
return list(
self.farmos_client.resource.iterate("quantity", self.farmos_quantity_type)
)
def get_target_object(self, key):
# fetch from cache, if applicable
if self.caches_target:
return super().get_target_object(key)
# okay now must fetch via API
if self.get_keys() != ["uuid"]:
raise ValueError("must use uuid key for this to work")
uuid = key[0]
try:
qty = self.farmos_client.resource.get_id(
"quantity", self.farmos_quantity_type, str(uuid)
)
except requests.HTTPError as exc:
if exc.response.status_code == 404:
return None
raise
return qty["data"]
def create_target_object(self, key, source_data):
if source_data.get("__ignoreme__"):
return None
if self.dry_run:
return source_data
payload = self.get_quantity_payload(source_data)
result = self.farmos_client.resource.send(
"quantity", self.farmos_quantity_type, payload
)
normal = self.normalize_target_object(result["data"])
normal["_new_object"] = result["data"]
return normal
def update_target_object(self, quantity, source_data, target_data=None):
if self.dry_run:
return quantity
payload = self.get_quantity_payload(source_data)
payload["id"] = str(source_data["uuid"])
result = self.farmos_client.resource.send(
"quantity", self.farmos_quantity_type, payload
)
return self.normalize_target_object(result["data"])
def normalize_target_object(self, qty):
result = {
"uuid": UUID(qty["id"]),
"measure": qty["attributes"]["measure"],
"value_numerator": qty["attributes"]["value"]["numerator"],
"value_denominator": qty["attributes"]["value"]["denominator"],
"label": qty["attributes"]["label"],
"quantity_type_uuid": UUID(
qty["relationships"]["quantity_type"]["data"]["id"]
),
"unit_uuid": None,
}
if unit := qty["relationships"]["units"]["data"]:
result["unit_uuid"] = UUID(unit["id"])
return result
def get_quantity_payload(self, source_data):
attrs = {}
if "measure" in self.fields:
attrs["measure"] = source_data["measure"]
if "value_numerator" in self.fields and "value_denominator" in self.fields:
attrs["value"] = {
"numerator": source_data["value_numerator"],
"denominator": source_data["value_denominator"],
}
if "label" in self.fields:
attrs["label"] = source_data["label"]
rels = {}
if "quantity_type_uuid" in self.fields:
rels["quantity_type"] = {
"data": {
"id": str(source_data["quantity_type_uuid"]),
"type": "quantity_type--quantity_type",
}
}
if "unit_uuid" in self.fields:
rels["units"] = {
"data": {
"id": str(source_data["unit_uuid"]),
"type": "taxonomy_term--unit",
}
}
payload = {"attributes": attrs, "relationships": rels}
return payload
class StandardQuantityImporter(ToFarmOSQuantity):
model_title = "StandardQuantity"
farmos_quantity_type = "standard"
############################## ##############################
# log importers # log importers
############################## ##############################
@ -590,20 +452,9 @@ class ToFarmOSLog(ToFarmOS):
supported_fields = [ supported_fields = [
"uuid", "uuid",
"name", "name",
"timestamp",
"is_movement",
"is_group_assignment",
"status",
"notes", "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): def get_target_objects(self, **kwargs):
result = self.farmos_client.log.get(self.farmos_log_type) result = self.farmos_client.log.get(self.farmos_log_type)
return result["data"] return result["data"]
@ -649,18 +500,14 @@ class ToFarmOSLog(ToFarmOS):
return self.normalize_target_object(result["data"]) return self.normalize_target_object(result["data"])
def normalize_target_object(self, log): def normalize_target_object(self, log):
normal = self.normal.normalize_farmos_log(log)
if notes := log["attributes"]["notes"]:
notes = notes["value"]
return { return {
"uuid": UUID(normal["uuid"]), "uuid": UUID(log["id"]),
"name": normal["name"], "name": log["attributes"]["name"],
"timestamp": self.app.make_utc(normal["timestamp"]), "notes": notes,
"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"]],
} }
def get_log_payload(self, source_data): def get_log_payload(self, source_data):
@ -668,43 +515,11 @@ class ToFarmOSLog(ToFarmOS):
attrs = {} attrs = {}
if "name" in self.fields: if "name" in self.fields:
attrs["name"] = source_data["name"] 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: if "notes" in self.fields:
attrs["notes"] = {"value": source_data["notes"]} attrs["notes"] = {"value": source_data["notes"]}
if "quick" in self.fields:
attrs["quick"] = source_data["quick"]
rels = {} payload = {"attributes": attrs}
if "assets" in self.fields:
assets = []
for asset_type, uuid in source_data["assets"]:
assets.append(
{
"type": f"asset--{asset_type}",
"id": str(uuid),
}
)
rels["asset"] = {"data": assets}
if "quantities" in self.fields:
quantities = []
for uuid in source_data["quantities"]:
quantities.append(
{
# TODO: support other quantity types
"type": "quantity--standard",
"id": str(uuid),
}
)
rels["quantity"] = {"data": quantities}
payload = {"attributes": attrs, "relationships": rels}
return payload return payload
@ -725,32 +540,6 @@ class MedicalLogImporter(ToFarmOSLog):
model_title = "MedicalLog" model_title = "MedicalLog"
farmos_log_type = "medical" 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): class ObservationLogImporter(ToFarmOSLog):

View file

@ -50,13 +50,10 @@ class ToFarmOSHandler(ImportHandler):
# TODO: a lot of duplication to cleanup here; see FromFarmOSHandler # 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. Establish the farmOS API client.
""" """
if client:
self.farmos_client = client
else:
token = self.get_farmos_oauth2_token() token = self.get_farmos_oauth2_token()
self.farmos_client = self.app.get_farmos_client(token=token) self.farmos_client = self.app.get_farmos_client(token=token)
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
@ -101,10 +98,8 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
importers["AnimalType"] = AnimalTypeImporter importers["AnimalType"] = AnimalTypeImporter
importers["AnimalAsset"] = AnimalAssetImporter importers["AnimalAsset"] = AnimalAssetImporter
importers["GroupAsset"] = GroupAssetImporter importers["GroupAsset"] = GroupAssetImporter
importers["PlantType"] = PlantTypeImporter
importers["PlantAsset"] = PlantAssetImporter importers["PlantAsset"] = PlantAssetImporter
importers["Unit"] = UnitImporter importers["Unit"] = UnitImporter
importers["StandardQuantity"] = StandardQuantityImporter
importers["ActivityLog"] = ActivityLogImporter importers["ActivityLog"] = ActivityLogImporter
importers["HarvestLog"] = HarvestLogImporter importers["HarvestLog"] = HarvestLogImporter
importers["MedicalLog"] = MedicalLogImporter importers["MedicalLog"] = MedicalLogImporter
@ -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): class PlantAssetImporter(FromWuttaFarm, farmos_importing.model.PlantAssetImporter):
""" """
WuttaFarm farmOS API exporter for Plant Assets 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 # log importers
############################## ##############################
@ -404,28 +334,14 @@ class FromWuttaFarmLog(FromWuttaFarm):
supported_fields = [ supported_fields = [
"uuid", "uuid",
"name", "name",
"timestamp",
"is_movement",
"is_group_assignment",
"status",
"notes", "notes",
"quick",
"assets",
"quantities",
] ]
def normalize_source_object(self, log): def normalize_source_object(self, log):
return { return {
"uuid": log.farmos_uuid or self.app.make_true_uuid(), "uuid": log.farmos_uuid or self.app.make_true_uuid(),
"name": log.message, "name": log.message,
"timestamp": log.timestamp,
"is_movement": log.is_movement,
"is_group_assignment": log.is_group_assignment,
"status": log.status,
"notes": log.notes, "notes": log.notes,
"quick": self.config.parse_list(log.quick) if log.quick else [],
"assets": [(a.asset_type, a.farmos_uuid) for a in log.assets],
"quantities": [qty.farmos_uuid for qty in log.quantities],
"_src_object": log, "_src_object": log,
} }
@ -453,24 +369,6 @@ class MedicalLogImporter(FromWuttaFarmLog, farmos_importing.model.MedicalLogImpo
source_model_class = model.MedicalLog 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( class ObservationLogImporter(
FromWuttaFarmLog, farmos_importing.model.ObservationLogImporter FromWuttaFarmLog, farmos_importing.model.ObservationLogImporter

View file

@ -46,13 +46,10 @@ class FromFarmOSHandler(ImportHandler):
source_key = "farmos" source_key = "farmos"
generic_source_title = "farmOS" generic_source_title = "farmOS"
def begin_source_transaction(self, client=None): def begin_source_transaction(self):
""" """
Establish the farmOS API client. Establish the farmOS API client.
""" """
if client:
self.farmos_client = client
else:
token = self.get_farmos_oauth2_token() token = self.get_farmos_oauth2_token()
self.farmos_client = self.app.get_farmos_client(token=token) self.farmos_client = self.app.get_farmos_client(token=token)
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
@ -187,7 +184,6 @@ class AssetImporterBase(FromFarmOS, ToWutta):
fields.extend( fields.extend(
[ [
"parents", "parents",
"owners",
] ]
) )
return fields return fields
@ -195,9 +191,8 @@ class AssetImporterBase(FromFarmOS, ToWutta):
def get_source_objects(self): def get_source_objects(self):
""" """ """ """
asset_type = self.get_farmos_asset_type() asset_type = self.get_farmos_asset_type()
return list( result = self.farmos_client.asset.get(asset_type)
self.farmos_client.asset.iterate(asset_type, params={"include": "image"}) return result["data"]
)
def normalize_source_data(self, **kwargs): def normalize_source_data(self, **kwargs):
""" """ """ """
@ -210,16 +205,10 @@ class AssetImporterBase(FromFarmOS, ToWutta):
return data 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
data["farmos_uuid"] = UUID(data.pop("uuid"))
data["asset_type"] = self.get_asset_type(asset)
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 relationships := asset.get("relationships"):
if image := relationships.get("image"): if image := relationships.get("image"):
@ -230,20 +219,35 @@ class AssetImporterBase(FromFarmOS, ToWutta):
if image_style := image["data"]["attributes"].get( if image_style := image["data"]["attributes"].get(
"image_style_uri" "image_style_uri"
): ):
data["image_url"] = image_style["large"] image_url = image_style["large"]
data["thumbnail_url"] = image_style["thumbnail"] thumbnail_url = image_style["thumbnail"]
if notes := asset["attributes"]["notes"]:
notes = notes["value"]
if self.farmos_4x:
archived = asset["attributes"]["archived"]
else:
archived = asset["attributes"]["status"] == "archived"
parents = None
if "parents" in self.fields: if "parents" in self.fields:
data["parents"] = [] parents = []
for parent in asset["relationships"]["parent"]["data"]: for parent in asset["relationships"]["parent"]["data"]:
data["parents"].append( parents.append((self.get_asset_type(parent), UUID(parent["id"])))
(self.get_asset_type(parent), UUID(parent["id"]))
)
if "owners" in self.fields: return {
data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]] "farmos_uuid": UUID(asset["id"]),
"drupal_id": asset["attributes"]["drupal_internal__id"],
return data "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): def get_asset_type(self, asset):
return asset["type"].split("--")[1] return asset["type"].split("--")[1]
@ -252,10 +256,10 @@ class AssetImporterBase(FromFarmOS, ToWutta):
data = super().normalize_target_object(asset) data = super().normalize_target_object(asset)
if "parents" in self.fields: if "parents" in self.fields:
data["parents"] = [(p.asset_type, p.farmos_uuid) for p in asset.parents] data["parents"] = [
(p.parent.asset_type, p.parent.farmos_uuid)
if "owners" in self.fields: for p in asset.asset._parents
data["owners"] = [user.farmos_uuid for user in asset.owners] ]
return data return data
@ -296,30 +300,6 @@ class AssetImporterBase(FromFarmOS, ToWutta):
) )
self.target_session.delete(parent) 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 return asset
@ -355,6 +335,11 @@ class AnimalAssetImporter(AssetImporterBase):
if animal_type.farmos_uuid: if animal_type.farmos_uuid:
self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type 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): def normalize_source_object(self, animal):
""" """ """ """
animal_type_uuid = None animal_type_uuid = None
@ -386,9 +371,10 @@ class AnimalAssetImporter(AssetImporterBase):
else: else:
sterile = animal["attributes"]["is_castrated"] sterile = animal["attributes"]["is_castrated"]
data = super().normalize_source_object(animal) data = self.normalize_asset(animal)
data.update( data.update(
{ {
"asset_type": "animal",
"animal_type_uuid": animal_type_uuid, "animal_type_uuid": animal_type_uuid,
"sex": animal["attributes"]["sex"], "sex": animal["attributes"]["sex"],
"is_sterile": sterile, "is_sterile": sterile,
@ -479,11 +465,17 @@ class GroupAssetImporter(AssetImporterBase):
"parents", "parents",
] ]
def get_source_objects(self):
""" """
groups = self.farmos_client.asset.get("group")
return groups["data"]
def normalize_source_object(self, group): def normalize_source_object(self, group):
""" """ """ """
data = super().normalize_source_object(group) data = self.normalize_asset(group)
data.update( data.update(
{ {
"asset_type": "group",
"produces_eggs": group["attributes"]["produces_eggs"], "produces_eggs": group["attributes"]["produces_eggs"],
} }
) )
@ -519,6 +511,11 @@ class LandAssetImporter(AssetImporterBase):
for land_type in self.target_session.query(model.LandType): for land_type in self.target_session.query(model.LandType):
self.land_types_by_id[land_type.drupal_id] = land_type 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): def normalize_source_object(self, land):
""" """ """ """
land_type_id = land["attributes"]["land_type"] land_type_id = land["attributes"]["land_type"]
@ -529,9 +526,10 @@ class LandAssetImporter(AssetImporterBase):
) )
return None return None
data = super().normalize_source_object(land) data = self.normalize_asset(land)
data.update( data.update(
{ {
"asset_type": "land",
"land_type_uuid": land_type.uuid, "land_type_uuid": land_type.uuid,
} }
) )
@ -624,7 +622,7 @@ class PlantAssetImporter(AssetImporterBase):
def normalize_source_object(self, plant): def normalize_source_object(self, plant):
""" """ """ """
plant_types = [] plant_types = None
if relationships := plant.get("relationships"): if relationships := plant.get("relationships"):
if plant_type := relationships.get("plant_type"): if plant_type := relationships.get("plant_type"):
@ -637,10 +635,11 @@ class PlantAssetImporter(AssetImporterBase):
else: else:
log.warning("plant type not found: %s", plant_type["id"]) log.warning("plant type not found: %s", plant_type["id"])
data = super().normalize_source_object(plant) data = self.normalize_asset(plant)
data.update( data.update(
{ {
"plant_types": set(plant_types), "asset_type": "plant",
"plant_types": plant_types,
} }
) )
return data return data
@ -649,7 +648,7 @@ class PlantAssetImporter(AssetImporterBase):
data = super().normalize_target_object(plant) data = super().normalize_target_object(plant)
if "plant_types" in self.fields: if "plant_types" in self.fields:
data["plant_types"] = set([pt.uuid for pt in plant.plant_types]) data["plant_types"] = [t.plant_type_uuid for t in plant._plant_types]
return data return data
@ -716,6 +715,11 @@ class StructureAssetImporter(AssetImporterBase):
for structure_type in self.target_session.query(model.StructureType): for structure_type in self.target_session.query(model.StructureType):
self.structure_types_by_id[structure_type.drupal_id] = structure_type 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): def normalize_source_object(self, structure):
""" """ """ """
structure_type_id = structure["attributes"]["structure_type"] structure_type_id = structure["attributes"]["structure_type"]
@ -728,9 +732,10 @@ class StructureAssetImporter(AssetImporterBase):
) )
return None return None
data = super().normalize_source_object(structure) data = self.normalize_asset(structure)
data.update( data.update(
{ {
"asset_type": "structure",
"structure_type_uuid": structure_type.uuid, "structure_type_uuid": structure_type.uuid,
} }
) )
@ -959,11 +964,8 @@ class LogImporterBase(FromFarmOS, ToWutta):
"log_type", "log_type",
"message", "message",
"timestamp", "timestamp",
"is_movement",
"is_group_assignment",
"notes", "notes",
"status", "status",
"quick",
] ]
) )
return fields return fields
@ -974,10 +976,6 @@ class LogImporterBase(FromFarmOS, ToWutta):
fields.extend( fields.extend(
[ [
"assets", "assets",
"groups",
"locations",
"quantities",
"owners",
] ]
) )
return fields return fields
@ -994,7 +992,6 @@ class LogImporterBase(FromFarmOS, ToWutta):
data["farmos_uuid"] = UUID(data.pop("uuid")) data["farmos_uuid"] = UUID(data.pop("uuid"))
data["message"] = data.pop("name") data["message"] = data.pop("name")
data["timestamp"] = self.app.make_utc(data["timestamp"]) data["timestamp"] = self.app.make_utc(data["timestamp"])
data["quick"] = ", ".join(data["quick"]) if data["quick"] else None
# TODO # TODO
data["log_type"] = self.get_farmos_log_type() 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"] (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 return data
def normalize_target_object(self, log): def normalize_target_object(self, log):
@ -1028,25 +1008,9 @@ class LogImporterBase(FromFarmOS, ToWutta):
if "assets" in self.fields: if "assets" in self.fields:
data["assets"] = [ 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 return data
def update_target_object(self, log, source_data, target_data=None): 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"]: for key in source_data["assets"]:
asset_type, farmos_uuid = key asset_type, farmos_uuid = key
if not target_data or key not in target_data["assets"]: if not target_data or key not in target_data["assets"]:
self.target_session.flush()
asset = ( asset = (
self.target_session.query(model.Asset) self.target_session.query(model.Asset)
.filter(model.Asset.asset_type == asset_type) .filter(model.Asset.asset_type == asset_type)
.filter(model.Asset.farmos_uuid == farmos_uuid) .filter(model.Asset.farmos_uuid == farmos_uuid)
.one() .one()
) )
log.assets.append(asset) log.log._assets.append(model.LogAsset(asset=asset))
if target_data: if target_data:
for key in target_data["assets"]: for key in target_data["assets"]:
@ -1077,108 +1042,13 @@ class LogImporterBase(FromFarmOS, ToWutta):
.filter(model.Asset.farmos_uuid == farmos_uuid) .filter(model.Asset.farmos_uuid == farmos_uuid)
.one() .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 = ( asset = (
self.target_session.query(model.Asset) self.target_session.query(model.LogAsset)
.filter(model.Asset.asset_type == asset_type) .filter(model.LogAsset.log == log)
.filter(model.Asset.farmos_uuid == farmos_uuid) .filter(model.LogAsset.asset == asset)
.one() .one()
) )
log.groups.append(asset) self.target_session.delete(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)
.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)
return log return log
@ -1190,6 +1060,17 @@ class ActivityLogImporter(LogImporterBase):
model_class = model.ActivityLog model_class = model.ActivityLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"notes",
"status",
"assets",
]
class HarvestLogImporter(LogImporterBase): class HarvestLogImporter(LogImporterBase):
""" """
@ -1198,6 +1079,17 @@ class HarvestLogImporter(LogImporterBase):
model_class = model.HarvestLog model_class = model.HarvestLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"notes",
"status",
"assets",
]
class MedicalLogImporter(LogImporterBase): class MedicalLogImporter(LogImporterBase):
""" """
@ -1206,16 +1098,16 @@ class MedicalLogImporter(LogImporterBase):
model_class = model.MedicalLog model_class = model.MedicalLog
def get_simple_fields(self): supported_fields = [
""" """ "farmos_uuid",
fields = list(super().get_simple_fields()) "drupal_id",
# nb. must explicitly declare proxy fields "log_type",
fields.extend( "message",
[ "timestamp",
"vet", "notes",
"status",
"assets",
] ]
)
return fields
class ObservationLogImporter(LogImporterBase): class ObservationLogImporter(LogImporterBase):
@ -1225,6 +1117,17 @@ class ObservationLogImporter(LogImporterBase):
model_class = model.ObservationLog model_class = model.ObservationLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"log_type",
"message",
"timestamp",
"notes",
"status",
"assets",
]
class QuantityImporterBase(FromFarmOS, ToWutta): class QuantityImporterBase(FromFarmOS, ToWutta):
""" """

View file

@ -84,40 +84,6 @@ class Normalizer(GenericHandler):
self._farmos_units = units self._farmos_units = units
return self._farmos_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={}): def normalize_farmos_log(self, log, included={}):
if timestamp := log["attributes"]["timestamp"]: if timestamp := log["attributes"]["timestamp"]:
@ -130,12 +96,8 @@ class Normalizer(GenericHandler):
log_type_object = {} log_type_object = {}
log_type_uuid = None log_type_uuid = None
asset_objects = [] asset_objects = []
group_objects = []
group_uuids = []
quantity_objects = [] quantity_objects = []
quantity_uuids = [] quantity_uuids = []
location_objects = []
location_uuids = []
owner_objects = [] owner_objects = []
owner_uuids = [] owner_uuids = []
if relationships := log.get("relationships"): if relationships := log.get("relationships"):
@ -170,54 +132,6 @@ class Normalizer(GenericHandler):
) )
asset_objects.append(asset_object) 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"): if quantities := relationships.get("quantity"):
for quantity in quantities["data"]: for quantity in quantities["data"]:
quantity_uuid = quantity["id"] quantity_uuid = quantity["id"]
@ -274,19 +188,12 @@ class Normalizer(GenericHandler):
"name": log["attributes"]["name"], "name": log["attributes"]["name"],
"timestamp": timestamp, "timestamp": timestamp,
"assets": asset_objects, "assets": asset_objects,
"groups": group_objects,
"group_uuids": group_uuids,
"quantities": quantity_objects, "quantities": quantity_objects,
"quantity_uuids": quantity_uuids, "quantity_uuids": quantity_uuids,
"is_group_assignment": log["attributes"]["is_group_assignment"], "is_group_assignment": log["attributes"]["is_group_assignment"],
"is_movement": log["attributes"]["is_movement"],
"quick": log["attributes"]["quick"], "quick": log["attributes"]["quick"],
"status": log["attributes"]["status"], "status": log["attributes"]["status"],
"notes": notes, "notes": notes,
"locations": location_objects,
"location_uuids": location_uuids,
"owners": owner_objects, "owners": owner_objects,
"owner_uuids": owner_uuids, "owner_uuids": owner_uuids,
# TODO: should we do this here or make caller do it?
"vet": log["attributes"].get("vet"),
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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

View file

@ -40,15 +40,6 @@ def main(global_config, **settings):
"wuttaweb:templates", "wuttaweb:templates",
], ],
) )
settings.setdefault(
"pyramid_deform.template_search_path",
" ".join(
[
"wuttafarm.web:templates/deform",
"wuttaweb:templates/deform",
]
),
)
# make config objects # make config objects
wutta_config = base.make_wutta_config(settings) wutta_config = base.make_wutta_config(settings)

View file

@ -27,7 +27,6 @@ import json
import colander import colander
from wuttaweb.db import Session
from wuttaweb.forms.schema import ObjectRef, WuttaSet from wuttaweb.forms.schema import ObjectRef, WuttaSet
from wuttaweb.forms.widgets import NotesWidget from wuttaweb.forms.widgets import NotesWidget
@ -56,12 +55,6 @@ class AnimalTypeRef(ObjectRef):
animal_type = obj animal_type = obj
return self.request.route_url("animal_types.view", uuid=animal_type.uuid) 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): class LogQuick(WuttaSet):
@ -77,31 +70,6 @@ class LogQuick(WuttaSet):
return LogQuickWidget(**kwargs) return LogQuickWidget(**kwargs)
class LogRef(ObjectRef):
"""
Custom schema type for a
:class:`~wuttafarm.db.model.log.Log` reference field.
This is a subclass of
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
"""
@property
def model_class(self): # pylint: disable=empty-docstring
""" """
model = self.app.model
return model.Log
def sort_query(self, query): # pylint: disable=empty-docstring
""" """
return query.order_by(self.model_class.message)
def get_object_url(self, obj): # pylint: disable=empty-docstring
""" """
log = obj
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
class FarmOSUnitRef(colander.SchemaType): class FarmOSUnitRef(colander.SchemaType):
def serialize(self, node, appstruct): def serialize(self, node, appstruct):
@ -217,6 +185,25 @@ class FarmOSQuantityRefs(WuttaSet):
return FarmOSQuantityRefsWidget(**kwargs) 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): class FarmOSPlantTypes(colander.SchemaType):
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):
@ -268,23 +255,13 @@ class PlantTypeRefs(WuttaSet):
def serialize(self, node, appstruct): def serialize(self, node, appstruct):
if not appstruct: if not appstruct:
return colander.null appstruct = []
uuids = [u.hex for u in appstruct]
return [uuid.hex for uuid in appstruct] return json.dumps(uuids)
def widget_maker(self, **kwargs): def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import PlantTypeRefsWidget 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) return PlantTypeRefsWidget(self.request, **kwargs)
@ -389,55 +366,21 @@ class AssetParentRefs(WuttaSet):
return AssetParentRefsWidget(self.request, **kwargs) return AssetParentRefsWidget(self.request, **kwargs)
class AssetRefs(WuttaSet): class LogAssetRefs(WuttaSet):
""" """
Schema type for Assets field (on a Log record) Schema type for Assets field (on a Log record)
""" """
def serialize(self, node, appstruct): def serialize(self, node, appstruct):
if not appstruct: if not appstruct:
return colander.null appstruct = []
uuids = [u.hex for u in appstruct]
return {asset.uuid for asset in appstruct} return json.dumps(uuids)
def widget_maker(self, **kwargs): def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import AssetRefsWidget from wuttafarm.web.forms.widgets import LogAssetRefsWidget
return AssetRefsWidget(self.request, **kwargs) return LogAssetRefsWidget(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)
class Notes(colander.String): class Notes(colander.String):

View file

@ -26,10 +26,10 @@ Custom form widgets for WuttaFarm
import json import json
import colander 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 webhelpers2.html import HTML, tags
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttafarm.web.util import render_quantity_objects from wuttafarm.web.util import render_quantity_objects
@ -228,6 +228,33 @@ class FarmOSUnitRefWidget(Widget):
return super().serialize(field, cstruct, **kw) 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): class FarmOSPlantTypesWidget(Widget):
""" """
Widget to display a farmOS "plant types" field. Widget to display a farmOS "plant types" field.
@ -258,40 +285,22 @@ class FarmOSPlantTypesWidget(Widget):
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
class PlantTypeRefsWidget(Widget): class PlantTypeRefsWidget(WuttaCheckboxChoiceWidget):
""" """
Widget for Plant Types field (on a Plant Asset). 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): def serialize(self, field, cstruct, **kw):
""" """ """ """
model = self.app.model model = self.app.model
session = Session() session = Session()
if cstruct in (colander.null, None): readonly = kw.get("readonly", self.readonly)
cstruct = () if readonly:
plant_types = []
if readonly := kw.get("readonly", self.readonly): for uuid in json.loads(cstruct):
items = [] plant_type = session.get(model.PlantType, uuid)
plant_types.append(
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(
HTML.tag( HTML.tag(
"li", "li",
c=tags.link_to( c=tags.link_to(
@ -302,34 +311,9 @@ class PlantTypeRefsWidget(Widget):
), ),
) )
) )
return HTML.tag("ul", c=plant_types)
return HTML.tag("ul", c=items) return super().serialize(field, cstruct, **kw)
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(","))
class StructureWidget(Widget): class StructureWidget(Widget):
@ -388,11 +372,6 @@ class UsersWidget(Widget):
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
##############################
# native data widgets
##############################
class AssetParentRefsWidget(WuttaCheckboxChoiceWidget): class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
""" """
Widget for Parents field which references assets. Widget for Parents field which references assets.
@ -424,9 +403,9 @@ class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
return super().serialize(field, cstruct, **kw) 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): def serialize(self, field, cstruct, **kw):
@ -437,7 +416,7 @@ class AssetRefsWidget(WuttaCheckboxChoiceWidget):
readonly = kw.get("readonly", self.readonly) readonly = kw.get("readonly", self.readonly)
if readonly: if readonly:
assets = [] assets = []
for uuid in cstruct or []: for uuid in json.loads(cstruct):
asset = session.get(model.Asset, uuid) asset = session.get(model.Asset, uuid)
assets.append( assets.append(
HTML.tag( HTML.tag(
@ -453,85 +432,3 @@ class AssetRefsWidget(WuttaCheckboxChoiceWidget):
return HTML.tag("ul", c=assets) return HTML.tag("ul", c=assets)
return super().serialize(field, cstruct, **kw) 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 ``<animal-type-picker>`` 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

View file

@ -72,7 +72,7 @@ class WuttaFarmMenuHandler(base.MenuHandler):
{ {
"title": "Eggs", "title": "Eggs",
"route": "quick.eggs", "route": "quick.eggs",
"perm": "quick.eggs", # "perm": "assets.list",
}, },
], ],
} }

View file

@ -14,30 +14,7 @@
</b-input> </b-input>
</b-field> </b-field>
<b-field grouped>
<b-field label="OAuth2 Client ID">
<b-input name="farmos.oauth2.client_id"
v-model="simpleSettings['farmos.oauth2.client_id']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
<b-field label="OAuth2 Scope">
<b-input name="farmos.oauth2.scope"
v-model="simpleSettings['farmos.oauth2.scope']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
<b-field label="OAuth2 Redirect URI">
<wutta-copyable-text text="${url('farmos_oauth_callback')}" />
</b-field>
<b-field label="farmOS Integration Mode"> <b-field label="farmOS Integration Mode">
<div style="display: flex; gap: 0.5rem; align-items: center;">
<b-select name="${app.appname}.farmos_integration_mode" <b-select name="${app.appname}.farmos_integration_mode"
v-model="simpleSettings['${app.appname}.farmos_integration_mode']" v-model="simpleSettings['${app.appname}.farmos_integration_mode']"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
@ -45,16 +22,6 @@
<option value="${value}">${label}</option> <option value="${value}">${label}</option>
% endfor % endfor
</b-select> </b-select>
<${b}-tooltip position="${'right' if request.use_oruga else 'is-right'}">
<b-icon pack="fas" icon="info-circle" type="is-warning" />
<template #content>
<p class="block">
<span class="has-text-weight-bold">RESTART IS REQUIRED</span>
if you change the integration mode.
</p>
</template>
</${b}-tooltip>
</div>
</b-field> </b-field>
<b-checkbox name="${app.appname}.farmos_style_grid_links" <b-checkbox name="${app.appname}.farmos_style_grid_links"

View file

@ -1,5 +1,4 @@
<%inherit file="wuttaweb:templates/base.mako" /> <%inherit file="wuttaweb:templates/base.mako" />
<%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" />
<%def name="index_title_controls()"> <%def name="index_title_controls()">
${parent.index_title_controls()} ${parent.index_title_controls()}
@ -15,8 +14,3 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${make_wuttafarm_components()}
</%def>

View file

@ -1,13 +0,0 @@
<div tal:define="
name name|field.name;
oid oid|field.oid;
vmodel vmodel|'modelData.'+oid;
can_create can_create|False;"
tal:omit-tag="">
<animal-type-picker tal:attributes="name name;
v-model vmodel;
:animal-types js_values;
:can-create str(can_create).lower();" />
</div>

View file

@ -1,13 +0,0 @@
<div tal:define="
name name|field.name;
oid oid|field.oid;
vmodel vmodel|'modelData.'+oid;
can_create can_create|False;"
tal:omit-tag="">
<plant-types-picker tal:attributes="name name;
v-model vmodel;
:plant-types js_values;
:can-create str(can_create).lower();" />
</div>

View file

@ -1,324 +0,0 @@
<%def name="make_wuttafarm_components()">
${self.make_animal_type_picker_component()}
${self.make_plant_types_picker_component()}
</%def>
<%def name="make_animal_type_picker_component()">
<script type="text/x-template" id="animal-type-picker-template">
<div>
<div style="display: flex; gap: 0.5rem;">
<b-select :name="name"
:value="internalValue"
@input="val => $emit('input', val)"
style="flex-grow: 1;">
<option v-for="atype in internalAnimalTypes"
:value="atype[0]">
{{ atype[1] }}
</option>
</b-select>
<b-button v-if="canCreate"
type="is-primary"
icon-pack="fas"
icon-left="plus"
@click="createInit()">
New
</b-button>
</div>
<${b}-modal v-if="canCreate"
has-modal-card
% if request.use_oruga:
v-model:active="createShowDialog"
% else:
:active.sync="createShowDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">New Animal Type</p>
</header>
<section class="modal-card-body">
<b-field label="Name" horizontal>
<b-input v-model="createName"
ref="createName"
expanded
@keydown.native="createNameKeydown" />
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="createSave()"
:disabled="createSaving || !createName"
icon-pack="fas"
icon-left="save">
{{ createSaving ? "Working, please wait..." : "Save" }}
</b-button>
<b-button @click="createShowDialog = false">
Cancel
</b-button>
</footer>
</div>
</${b}-modal>
</div>
</script>
<script>
const AnimalTypePicker = {
template: '#animal-type-picker-template',
mixins: [WuttaRequestMixin],
props: {
name: String,
value: String,
animalTypes: Array,
canCreate: Boolean,
},
data() {
return {
internalAnimalTypes: this.animalTypes,
internalValue: this.value,
createShowDialog: false,
createName: null,
createSaving: false,
}
},
methods: {
createInit(name) {
this.createName = name || null
this.createShowDialog = true
this.$nextTick(() => {
this.$refs.createName.focus()
})
},
createNameKeydown(event) {
// nb. must prevent main form submit on ENTER
// (since ultimately this lives within an outer form)
// but also we can submit the modal pseudo-form
if (event.which == 13) {
event.preventDefault()
this.createSave()
}
},
createSave() {
this.createSaving = true
const url = "${url('animal_types.ajax_create')}"
const params = {name: this.createName}
this.wuttaPOST(url, params, response => {
this.internalAnimalTypes.push([response.data.uuid, response.data.name])
this.$nextTick(() => {
this.internalValue = response.data.uuid
this.createSaving = false
this.createShowDialog = false
})
}, response => {
this.createSaving = false
})
},
},
}
Vue.component('animal-type-picker', AnimalTypePicker)
<% request.register_component('animal-type-picker', 'AnimalTypePicker') %>
</script>
</%def>
<%def name="make_plant_types_picker_component()">
<script type="text/x-template" id="plant-types-picker-template">
<div>
<input type="hidden" :name="name" :value="value" />
<div style="display: flex; gap: 0.5rem; align-items: center;">
<span>Add:</span>
<b-autocomplete v-model="addName"
ref="addName"
:data="addNameData"
field="name"
open-on-focus
keep-first
@select="addNameSelected"
clear-on-select
style="flex-grow: 1;">
<template #empty>No results found</template>
</b-autocomplete>
<b-button type="is-primary"
icon-pack="fas"
icon-left="plus"
@click="createInit()">
New
</b-button>
</div>
<${b}-table :data="plantTypeData">
<${b}-table-column field="name" v-slot="props">
<span>{{ props.row.name }}</span>
</${b}-table-column>
<${b}-table-column v-slot="props">
<a href="#"
class="has-text-danger"
@click.prevent="removePlantType(props.row)">
<i class="fas fa-trash" /> &nbsp; Remove
</a>
</${b}-table-column>
</${b}-table>
<${b}-modal v-if="canCreate"
has-modal-card
% if request.use_oruga:
v-model:active="createShowDialog"
% else:
:active.sync="createShowDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">New Plant Type</p>
</header>
<section class="modal-card-body">
<b-field label="Name" horizontal>
<b-input v-model="createName"
ref="createName"
expanded
@keydown.native="createNameKeydown" />
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="createSave()"
:disabled="createSaving || !createName"
icon-pack="fas"
icon-left="save">
{{ createSaving ? "Working, please wait..." : "Save" }}
</b-button>
<b-button @click="createShowDialog = false">
Cancel
</b-button>
</footer>
</div>
</${b}-modal>
</div>
</script>
<script>
const PlantTypesPicker = {
template: '#plant-types-picker-template',
mixins: [WuttaRequestMixin],
props: {
name: String,
value: Array,
plantTypes: Array,
canCreate: Boolean,
},
data() {
return {
internalPlantTypes: this.plantTypes.map((pt) => {
return {uuid: pt[0], name: pt[1]}
}),
addShowDialog: false,
addName: '',
createShowDialog: false,
createName: null,
createSaving: false,
}
},
computed: {
plantTypeData() {
const data = []
if (this.value) {
for (let ptype of this.internalPlantTypes) {
// ptype = {uuid: ptype[0], name: ptype[1]}
if (this.value.includes(ptype.uuid)) {
data.push(ptype)
}
}
}
return data
},
addNameData() {
if (!this.addName) {
return this.internalPlantTypes
}
return this.internalPlantTypes.filter((ptype) => {
return ptype.name.toLowerCase().indexOf(this.addName.toLowerCase()) >= 0
})
},
},
methods: {
addNameSelected(option) {
const value = Array.from(this.value || [])
if (!value.includes(option.uuid)) {
value.push(option.uuid)
this.$emit('input', value)
}
this.addName = null
},
createInit() {
this.createName = this.addName
this.createShowDialog = true
this.$nextTick(() => {
this.$refs.createName.focus()
})
},
createNameKeydown(event) {
// nb. must prevent main form submit on ENTER
// (since ultimately this lives within an outer form)
// but also we can submit the modal pseudo-form
if (event.which == 13) {
event.preventDefault()
this.createSave()
}
},
createSave() {
this.createSaving = true
const url = "${url('plant_types.ajax_create')}"
const params = {name: this.createName}
this.wuttaPOST(url, params, response => {
this.internalPlantTypes.push(response.data)
const value = Array.from(this.value || [])
value.push(response.data.uuid)
this.$emit('input', value)
this.addName = null
this.createSaving = false
this.createShowDialog = false
}, response => {
this.createSaving = false
})
},
removePlantType(ptype) {
let value = Array.from(this.value)
const i = value.indexOf(ptype.uuid)
value.splice(i, 1)
this.$emit('input', value)
},
},
}
Vue.component('plant-types-picker', PlantTypesPicker)
<% request.register_component('plant-types-picker', 'PlantTypesPicker') %>
</script>
</%def>

View file

@ -23,29 +23,9 @@
Misc. utilities for web app Misc. utilities for web app
""" """
from pyramid import httpexceptions
from webhelpers2.html import HTML 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): def save_farmos_oauth2_token(request, token):
""" """
Common logic for saving the given OAuth2 token within the user Common logic for saving the given OAuth2 token within the user

View file

@ -26,13 +26,11 @@ Master view for Animals
from webhelpers2.html import tags from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.util import get_form_data
from wuttafarm.db.model import AnimalType, AnimalAsset from wuttafarm.db.model import AnimalType, AnimalAsset
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
from wuttafarm.web.forms.schema import AnimalTypeRef from wuttafarm.web.forms.schema import AnimalTypeRef
from wuttafarm.web.forms.widgets import ImageWidget from wuttafarm.web.forms.widgets import ImageWidget
from wuttafarm.web.util import get_farmos_client_for_user
class AnimalTypeView(AssetTypeMasterView): class AnimalTypeView(AssetTypeMasterView):
@ -44,8 +42,6 @@ class AnimalTypeView(AssetTypeMasterView):
route_prefix = "animal_types" route_prefix = "animal_types"
url_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" farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
grid_columns = [ grid_columns = [
@ -62,8 +58,8 @@ class AnimalTypeView(AssetTypeMasterView):
form_fields = [ form_fields = [
"name", "name",
"description", "description",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
has_rows = True has_rows = True
@ -107,19 +103,6 @@ class AnimalTypeView(AssetTypeMasterView):
return buttons 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): def get_row_grid_data(self, animal_type):
model = self.app.model model = self.app.model
session = self.Session() session = self.Session()
@ -142,7 +125,6 @@ class AnimalTypeView(AssetTypeMasterView):
# sex # sex
g.set_enum("sex", enum.ANIMAL_SEX) g.set_enum("sex", enum.ANIMAL_SEX)
g.filters["sex"].verbs = ["equal", "not_equal"]
# archived # archived
g.set_renderer("archived", "boolean") g.set_renderer("archived", "boolean")
@ -152,55 +134,6 @@ class AnimalTypeView(AssetTypeMasterView):
def get_row_action_url_view(self, animal, i): def get_row_action_url_view(self, animal, i):
return self.request.route_url("animal_assets.view", uuid=animal.uuid) 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): class AnimalAssetView(AssetMasterView):
""" """
@ -212,7 +145,6 @@ class AnimalAssetView(AssetMasterView):
url_prefix = "/assets/animal" url_prefix = "/assets/animal"
farmos_refurl_path = "/assets/animal" farmos_refurl_path = "/assets/animal"
farmos_bundle = "animal"
labels = { labels = {
"animal_type": "Species/Breed", "animal_type": "Species/Breed",
@ -228,9 +160,6 @@ class AnimalAssetView(AssetMasterView):
"birthdate", "birthdate",
"is_sterile", "is_sterile",
"sex", "sex",
"groups",
"owners",
"locations",
"archived", "archived",
] ]
@ -243,12 +172,9 @@ class AnimalAssetView(AssetMasterView):
"is_sterile", "is_sterile",
"notes", "notes",
"asset_type", "asset_type",
"owners",
"locations",
"groups",
"archived", "archived",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
"thumbnail_url", "thumbnail_url",
"image_url", "image_url",
"thumbnail", "thumbnail",
@ -275,7 +201,6 @@ class AnimalAssetView(AssetMasterView):
# sex # sex
g.set_enum("sex", enum.ANIMAL_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): def render_animal_type_for_grid(self, animal, field, value):
url = self.request.route_url("animal_types.view", uuid=animal.animal_type_uuid) 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)) f.set_node("animal_type", AnimalTypeRef(self.request))
# sex # 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 pass # TODO: dict enum widget does not handle null values well
else: else:
f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX)) f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX))

View file

@ -38,7 +38,6 @@ class AssetTypeView(WuttaFarmMasterView):
grid_columns = [ grid_columns = [
"name", "name",
"drupal_id",
"description", "description",
] ]
@ -51,8 +50,8 @@ class AssetTypeView(WuttaFarmMasterView):
form_fields = [ form_fields = [
"name", "name",
"description", "description",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
def configure_grid(self, grid): def configure_grid(self, grid):
@ -79,19 +78,6 @@ class AssetTypeView(WuttaFarmMasterView):
return buttons 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): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -25,17 +25,13 @@ Master view for Assets
from collections import OrderedDict from collections import OrderedDict
from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Asset, Log 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.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): def get_asset_type_enum(config):
@ -49,6 +45,79 @@ def get_asset_type_enum(config):
return asset_types 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): class AssetTypeMasterView(WuttaFarmMasterView):
""" """
Base class for "Asset Type" master views. Base class for "Asset Type" master views.
@ -64,12 +133,6 @@ class AssetMasterView(WuttaFarmMasterView):
Base class for Asset master views Base class for Asset master views
""" """
farmos_entity_type = "asset"
labels = {
"groups": "Group Membership",
}
sort_defaults = "asset_name" sort_defaults = "asset_name"
filter_defaults = { filter_defaults = {
@ -112,10 +175,7 @@ class AssetMasterView(WuttaFarmMasterView):
model = self.app.model model = self.app.model
model_class = self.get_model_class() model_class = self.get_model_class()
session = session or self.Session() session = session or self.Session()
query = session.query(model_class) return session.query(model_class).join(model.Asset)
if model_class is not model.Asset:
query = query.join(model.Asset)
return query
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
@ -140,77 +200,15 @@ class AssetMasterView(WuttaFarmMasterView):
# parents # parents
g.set_renderer("parents", self.render_parents_for_grid) 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 # archived
g.set_renderer("archived", "boolean") g.set_renderer("archived", "boolean")
g.set_sorter("archived", model.Asset.archived) g.set_sorter("archived", model.Asset.archived)
g.set_filter("archived", model.Asset.archived) g.set_filter("archived", model.Asset.archived)
def render_parents_for_grid(self, asset, field, value): def render_parents_for_grid(self, asset, field, value):
parents = [str(p.parent) for p in asset.asset._parents]
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]
return ", ".join(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): def grid_row_class(self, asset, data, i):
""" """ """ """
if asset.archived: if asset.archived:
@ -220,7 +218,6 @@ class AssetMasterView(WuttaFarmMasterView):
def configure_form(self, form): def configure_form(self, form):
f = form f = form
super().configure_form(f) super().configure_form(f)
asset_handler = self.app.get_asset_handler()
asset = form.model_instance asset = form.model_instance
# asset_type # asset_type
@ -233,39 +230,12 @@ class AssetMasterView(WuttaFarmMasterView):
) )
f.set_readonly("asset_type") 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 # parents
if self.creating or self.editing: if self.creating or self.editing:
f.remove("parents") # TODO: add support for this f.remove("parents") # TODO: add support for this
else: else:
f.set_node("parents", AssetParentRefs(self.request)) 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 # notes
f.set_widget("notes", "notes") f.set_widget("notes", "notes")
@ -296,14 +266,11 @@ class AssetMasterView(WuttaFarmMasterView):
asset = super().objectify(form) asset = super().objectify(form)
if self.creating: 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 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): def get_farmos_url(self, asset):
return self.app.get_farmos_url(f"/asset/{asset.drupal_id}") return self.app.get_farmos_url(f"/asset/{asset.drupal_id}")
@ -311,7 +278,7 @@ class AssetMasterView(WuttaFarmMasterView):
buttons = super().get_xref_buttons(asset) buttons = super().get_xref_buttons(asset)
if asset.farmos_uuid: 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" route = f"farmos_{asset_type}_assets.view"
url = self.request.route_url(route, uuid=asset.farmos_uuid) url = self.request.route_url(route, uuid=asset.farmos_uuid)
buttons.append( buttons.append(
@ -322,21 +289,6 @@ class AssetMasterView(WuttaFarmMasterView):
return buttons 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): def get_row_grid_data(self, asset):
model = self.app.model model = self.app.model
session = self.Session() session = self.Session()
@ -349,12 +301,7 @@ class AssetMasterView(WuttaFarmMasterView):
def configure_row_grid(self, grid): def configure_row_grid(self, grid):
g = grid g = grid
super().configure_row_grid(g) super().configure_row_grid(g)
enum = self.app.enum
model = self.app.model model = self.app.model
session = self.Session()
# status
g.set_enum("status", enum.LOG_STATUS)
# drupal_id # drupal_id
g.set_label("drupal_id", "ID", column_only=True) g.set_label("drupal_id", "ID", column_only=True)
@ -371,62 +318,16 @@ class AssetMasterView(WuttaFarmMasterView):
# log_type # log_type
g.set_sorter("log_type", model.Log.log_type) g.set_sorter("log_type", model.Log.log_type)
g.set_filter("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): def get_row_action_url_view(self, log, i):
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid) 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): def defaults(config, **kwargs):
base = globals() base = globals()
AllAssetView = kwargs.get("AllAssetView", base["AllAssetView"]) AssetView = kwargs.get("AssetView", base["AssetView"])
AllAssetView.defaults(config) AssetView.defaults(config)
def includeme(config): def includeme(config):

View file

@ -55,10 +55,9 @@ class AuthView(base.AuthView):
return None return None
def get_farmos_oauth2_session(self): def get_farmos_oauth2_session(self):
farmos = self.app.get_farmos_handler()
return OAuth2Session( return OAuth2Session(
client_id=farmos.get_oauth2_client_id(), client_id="farm",
scope=farmos.get_oauth2_scope(), scope="farm_manager",
redirect_uri=self.request.route_url("farmos_oauth_callback"), redirect_uri=self.request.route_url("farmos_oauth_callback"),
) )

View file

@ -87,20 +87,10 @@ class CommonView(base.CommonView):
"farmos_logs_medical.view", "farmos_logs_medical.view",
"farmos_logs_observation.list", "farmos_logs_observation.list",
"farmos_logs_observation.view", "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.list",
"farmos_structure_assets.view", "farmos_structure_assets.view",
"farmos_structure_types.list", "farmos_structure_types.list",
"farmos_structure_types.view", "farmos_structure_types.view",
"farmos_units.list",
"farmos_units.view",
"farmos_users.list", "farmos_users.list",
"farmos_users.view", "farmos_users.view",
"group_assets.create", "group_assets.create",
@ -131,7 +121,6 @@ class CommonView(base.CommonView):
"logs_observation.list", "logs_observation.list",
"logs_observation.view", "logs_observation.view",
"logs_observation.versions", "logs_observation.versions",
"quick.eggs",
"structure_types.list", "structure_types.list",
"structure_types.view", "structure_types.view",
"structure_types.versions", "structure_types.versions",

View file

@ -39,7 +39,7 @@ from wuttafarm.web.grids import (
NullableBooleanFilter, NullableBooleanFilter,
DateTimeFilter, DateTimeFilter,
) )
from wuttafarm.web.forms.schema import FarmOSRef, FarmOSAssetRefs from wuttafarm.web.forms.schema import FarmOSRef
class AnimalView(AssetMasterView): class AnimalView(AssetMasterView):
@ -87,9 +87,9 @@ class AnimalView(AssetMasterView):
"is_sterile", "is_sterile",
"notes", "notes",
"asset_type_name", "asset_type_name",
"groups",
"owners", "owners",
"locations", "locations",
"groups",
"archived", "archived",
"thumbnail_url", "thumbnail_url",
"image_url", "image_url",
@ -147,7 +147,7 @@ class AnimalView(AssetMasterView):
def render_groups_for_grid(self, animal, field, value): def render_groups_for_grid(self, animal, field, value):
groups = [] groups = []
for group in animal["groups"]: for group in animal["group_objects"]:
if self.farmos_style_grid_links: if self.farmos_style_grid_links:
url = self.request.route_url( url = self.request.route_url(
"farmos_group_assets.view", uuid=group["uuid"] "farmos_group_assets.view", uuid=group["uuid"]
@ -209,7 +209,6 @@ class AnimalView(AssetMasterView):
group = { group = {
"uuid": group["id"], "uuid": group["id"],
"name": group["attributes"]["name"], "name": group["attributes"]["name"],
"asset_type": "group",
} }
group_objects.append(group) group_objects.append(group)
group_names.append(group["name"]) group_names.append(group["name"])
@ -219,7 +218,7 @@ class AnimalView(AssetMasterView):
"animal_type": animal_type_object, "animal_type": animal_type_object,
"animal_type_uuid": animal_type_object["uuid"], "animal_type_uuid": animal_type_object["uuid"],
"animal_type_name": animal_type_object["name"], "animal_type_name": animal_type_object["name"],
"groups": group_objects, "group_objects": group_objects,
"group_names": group_names, "group_names": group_names,
"birthdate": birthdate, "birthdate": birthdate,
"sex": animal["attributes"]["sex"] or colander.null, "sex": animal["attributes"]["sex"] or colander.null,
@ -274,8 +273,6 @@ class AnimalView(AssetMasterView):
# groups # groups
if self.creating or self.editing: if self.creating or self.editing:
f.remove("groups") # TODO f.remove("groups") # TODO
else:
f.set_node("groups", FarmOSAssetRefs(self.request))
def get_api_payload(self, animal): def get_api_payload(self, animal):
payload = super().get_api_payload(animal) payload = super().get_api_payload(animal)

View file

@ -53,8 +53,8 @@ class AssetMasterView(FarmOSMasterView):
labels = { labels = {
"name": "Asset Name", "name": "Asset Name",
"asset_type_name": "Asset Type", "asset_type_name": "Asset Type",
"owners": "Owner",
"locations": "Location", "locations": "Location",
"groups": "Group Membership",
"thumbnail_url": "Thumbnail URL", "thumbnail_url": "Thumbnail URL",
"image_url": "Image URL", "image_url": "Image URL",
} }
@ -104,7 +104,6 @@ class AssetMasterView(FarmOSMasterView):
g.set_filter("name", StringFilter) g.set_filter("name", StringFilter)
# owners # owners
g.set_label("owners", "Owner")
g.set_renderer("owners", self.render_owners_for_grid) g.set_renderer("owners", self.render_owners_for_grid)
# locations # locations
@ -240,7 +239,6 @@ class AssetMasterView(FarmOSMasterView):
if self.creating or self.editing: if self.creating or self.editing:
f.remove("locations") f.remove("locations")
else: else:
f.set_label("locations", "Current Location")
f.set_node("locations", FarmOSLocationRefs(self.request)) f.set_node("locations", FarmOSLocationRefs(self.request))
# owners # owners

View file

@ -23,7 +23,6 @@
View for farmOS Harvest Logs View for farmOS Harvest Logs
""" """
import colander
from webhelpers2.html import tags from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum
@ -60,7 +59,6 @@ class LogMasterView(FarmOSMasterView):
labels = { labels = {
"name": "Log Name", "name": "Log Name",
"log_type_name": "Log Type", "log_type_name": "Log Type",
"locations": "Location",
"quantities": "Quantity", "quantities": "Quantity",
} }
@ -70,7 +68,6 @@ class LogMasterView(FarmOSMasterView):
"timestamp", "timestamp",
"name", "name",
"assets", "assets",
"locations",
"quantities", "quantities",
"is_group_assignment", "is_group_assignment",
"owners", "owners",
@ -87,21 +84,17 @@ class LogMasterView(FarmOSMasterView):
"name", "name",
"timestamp", "timestamp",
"assets", "assets",
"groups",
"locations",
"quantities", "quantities",
"notes", "notes",
"status", "status",
"log_type_name", "log_type_name",
"owners", "owners",
"is_movement",
"is_group_assignment",
"quick", "quick",
"drupal_id", "drupal_id",
] ]
def get_farmos_api_includes(self): 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): def get_grid_data(self, **kwargs):
return ResourceData( return ResourceData(
@ -146,12 +139,6 @@ class LogMasterView(FarmOSMasterView):
# assets # assets
g.set_renderer("assets", self.render_assets_for_grid) 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 # quantities
g.set_renderer("quantities", self.render_quantities_for_grid) 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) g.set_renderer("owners", self.render_owners_for_grid)
def render_assets_for_grid(self, log, field, value): def render_assets_for_grid(self, log, field, value):
if not value:
return ""
assets = [] assets = []
for asset in value: for asset in value:
if self.farmos_style_grid_links: if self.farmos_style_grid_links:
@ -226,21 +210,9 @@ class LogMasterView(FarmOSMasterView):
# assets # assets
f.set_node("assets", FarmOSAssetRefs(self.request)) f.set_node("assets", FarmOSAssetRefs(self.request))
# groups
f.set_node("groups", FarmOSAssetRefs(self.request))
# locations
f.set_node("locations", FarmOSAssetRefs(self.request))
# quantities # quantities
f.set_node("quantities", FarmOSQuantityRefs(self.request)) 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 # notes
f.set_node("notes", Notes()) f.set_node("notes", Notes())

View file

@ -48,6 +48,7 @@ class HarvestLogView(LogMasterView):
"name", "name",
"assets", "assets",
"quantities", "quantities",
"is_group_assignment",
"owners", "owners",
] ]

View file

@ -24,7 +24,6 @@ View for farmOS Medical Logs
""" """
from wuttafarm.web.views.farmos.logs import LogMasterView from wuttafarm.web.views.farmos.logs import LogMasterView
from wuttafarm.web.grids import SimpleSorter, StringFilter
class MedicalLogView(LogMasterView): class MedicalLogView(LogMasterView):
@ -42,35 +41,6 @@ class MedicalLogView(LogMasterView):
farmos_log_type = "medical" farmos_log_type = "medical"
farmos_refurl_path = "/logs/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): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -41,18 +41,6 @@ class ObservationLogView(LogMasterView):
farmos_log_type = "observation" farmos_log_type = "observation"
farmos_refurl_path = "/logs/observation" farmos_refurl_path = "/logs/observation"
grid_columns = [
"status",
"drupal_id",
"timestamp",
"name",
"assets",
"locations",
"groups",
"is_group_assignment",
"owners",
]
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -34,7 +34,7 @@ from wuttaweb.views import MasterView
from wuttaweb.forms.schema import WuttaDateTime from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget 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 ( from wuttafarm.web.grids import (
ResourceData, ResourceData,
StringFilter, StringFilter,
@ -70,12 +70,28 @@ class FarmOSMasterView(MasterView):
def __init__(self, request, context=None): def __init__(self, request, context=None):
super().__init__(request, context=context) super().__init__(request, context=context)
self.farmos_client = get_farmos_client_for_user(self.request) self.farmos_client = self.get_farmos_client()
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
self.normal = self.app.get_normalizer(self.farmos_client) self.normal = self.app.get_normalizer(self.farmos_client)
self.raw_json = None self.raw_json = None
self.farmos_style_grid_links = use_farmos_style_grid_links(self.config) 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): def get_fallback_templates(self, template):
""" """ """ """
templates = super().get_fallback_templates(template) templates = super().get_fallback_templates(template)

View file

@ -32,7 +32,6 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views.farmos import FarmOSMasterView from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import FarmOSUnitRef from wuttafarm.web.forms.schema import FarmOSUnitRef
from wuttafarm.web.grids import ResourceData
class QuantityTypeView(FarmOSMasterView): class QuantityTypeView(FarmOSMasterView):
@ -131,15 +130,13 @@ class QuantityMasterView(FarmOSMasterView):
farmos_quantity_type = None farmos_quantity_type = None
grid_columns = [ grid_columns = [
"drupal_id",
"as_text",
"measure", "measure",
"value", "value",
"unit",
"label", "label",
"changed",
] ]
sort_defaults = ("drupal_id", "desc") sort_defaults = ("changed", "desc")
form_fields = [ form_fields = [
"measure", "measure",
@ -150,58 +147,20 @@ class QuantityMasterView(FarmOSMasterView):
"changed", "changed",
] ]
def get_farmos_api_includes(self): def get_grid_data(self, columns=None, session=None):
return {"units"} result = self.farmos_client.resource.get("quantity", self.farmos_quantity_type)
return [self.normalize_quantity(t) for t in result["data"]]
def get_grid_data(self, **kwargs):
return ResourceData(
self.config,
self.farmos_client,
f"quantity--{self.farmos_quantity_type}",
include=",".join(self.get_farmos_api_includes()),
normalizer=self.normalize_quantity,
)
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
super().configure_grid(g) super().configure_grid(g)
# drupal_id
g.set_label("drupal_id", "ID", column_only=True)
# as_text
g.set_renderer("as_text", self.render_as_text_for_grid)
# measure
g.set_renderer("measure", self.render_measure_for_grid)
# value # value
g.set_renderer("value", self.render_value_for_grid) g.set_link("value")
# unit
g.set_renderer("unit", self.render_unit_for_grid)
# changed # changed
g.set_renderer("changed", "datetime") g.set_renderer("changed", "datetime")
def render_as_text_for_grid(self, qty, field, value):
measure = qty["measure"].capitalize()
value = qty["value"]["decimal"]
units = qty["unit"]["name"] if qty["unit"] else "??"
return f"( {measure} ) {value} {units}"
def render_measure_for_grid(self, qty, field, value):
return qty["measure"].capitalize()
def render_unit_for_grid(self, qty, field, value):
unit = qty[field]
if not unit:
return ""
return unit["name"]
def render_value_for_grid(self, qty, field, value):
return qty["value"]["decimal"]
def get_instance(self): def get_instance(self):
quantity = self.farmos_client.resource.get_id( quantity = self.farmos_client.resource.get_id(
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"] "quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
@ -228,7 +187,7 @@ class QuantityMasterView(FarmOSMasterView):
def get_instance_title(self, quantity): def get_instance_title(self, quantity):
return quantity["value"] return quantity["value"]
def normalize_quantity(self, quantity, included={}): def normalize_quantity(self, quantity):
if created := quantity["attributes"]["created"]: if created := quantity["attributes"]["created"]:
created = datetime.datetime.fromisoformat(created) created = datetime.datetime.fromisoformat(created)
@ -238,37 +197,11 @@ class QuantityMasterView(FarmOSMasterView):
changed = datetime.datetime.fromisoformat(changed) changed = datetime.datetime.fromisoformat(changed)
changed = self.app.localtime(changed) changed = self.app.localtime(changed)
quantity_type_object = None
quantity_type_uuid = None
unit_object = None
unit_uuid = None
if relationships := quantity["relationships"]:
if quantity_type := relationships["quantity_type"]["data"]:
quantity_type_uuid = quantity_type["id"]
quantity_type_object = {
"uuid": quantity_type_uuid,
"type": "quantity_type--quantity_type",
}
if unit := relationships["units"]["data"]:
unit_uuid = unit["id"]
if unit := included.get(unit_uuid):
unit_object = {
"uuid": unit_uuid,
"type": "taxonomy_term--unit",
"name": unit["attributes"]["name"],
}
return { return {
"uuid": quantity["id"], "uuid": quantity["id"],
"drupal_id": quantity["attributes"]["drupal_internal__id"], "drupal_id": quantity["attributes"]["drupal_internal__id"],
"quantity_type": quantity_type_object,
"quantity_type_uuid": quantity_type_uuid,
"measure": quantity["attributes"]["measure"], "measure": quantity["attributes"]["measure"],
"value": quantity["attributes"]["value"], "value": quantity["attributes"]["value"],
"unit": unit_object,
"unit_uuid": unit_uuid,
"label": quantity["attributes"]["label"] or colander.null, "label": quantity["attributes"]["label"] or colander.null,
"created": created, "created": created,
"changed": changed, "changed": changed,

View file

@ -37,7 +37,6 @@ class GroupView(AssetMasterView):
url_prefix = "/assets/group" url_prefix = "/assets/group"
farmos_refurl_path = "/assets/group" farmos_refurl_path = "/assets/group"
farmos_bundle = "group"
grid_columns = [ grid_columns = [
"thumbnail", "thumbnail",
@ -53,8 +52,8 @@ class GroupView(AssetMasterView):
"asset_type", "asset_type",
"produces_eggs", "produces_eggs",
"archived", "archived",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]

View file

@ -51,8 +51,8 @@ class LandTypeView(AssetTypeMasterView):
form_fields = [ form_fields = [
"name", "name",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
has_rows = True has_rows = True
@ -129,19 +129,6 @@ class LandTypeView(AssetTypeMasterView):
def get_row_action_url_view(self, land_asset, i): def get_row_action_url_view(self, land_asset, i):
return self.request.route_url("land_assets.view", uuid=land_asset.uuid) 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): class LandAssetView(AssetMasterView):
""" """
@ -152,7 +139,6 @@ class LandAssetView(AssetMasterView):
route_prefix = "land_assets" route_prefix = "land_assets"
url_prefix = "/assets/land" url_prefix = "/assets/land"
farmos_bundle = "land"
farmos_refurl_path = "/assets/land" farmos_refurl_path = "/assets/land"
grid_columns = [ grid_columns = [
@ -173,8 +159,8 @@ class LandAssetView(AssetMasterView):
"is_location", "is_location",
"is_fixed", "is_fixed",
"archived", "archived",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
def configure_grid(self, grid): def configure_grid(self, grid):

View file

@ -26,7 +26,6 @@ Base views for Logs
from collections import OrderedDict from collections import OrderedDict
import colander import colander
from webhelpers2.html import tags, HTML
from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.db import Session from wuttaweb.db import Session
@ -34,8 +33,18 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import LogType, Log from wuttafarm.db.model import LogType, Log
from wuttafarm.web.forms.schema import AssetRefs, LogQuantityRefs, OwnerRefs from wuttafarm.web.forms.schema import LogAssetRefs
from wuttafarm.util import get_log_type_enum
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): class LogTypeView(WuttaFarmMasterView):
@ -61,8 +70,8 @@ class LogTypeView(WuttaFarmMasterView):
form_fields = [ form_fields = [
"name", "name",
"description", "description",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
def configure_grid(self, grid): def configure_grid(self, grid):
@ -90,17 +99,85 @@ class LogTypeView(WuttaFarmMasterView):
return buttons 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): class LogMasterView(WuttaFarmMasterView):
""" """
Base class for Asset master views Base class for Asset master views
""" """
farmos_entity_type = "log"
labels = { labels = {
"message": "Log Name", "message": "Log Name",
"locations": "Location", "owners": "Owner",
"quantities": "Quantity",
} }
grid_columns = [ grid_columns = [
@ -109,8 +186,8 @@ class LogMasterView(WuttaFarmMasterView):
"timestamp", "timestamp",
"message", "message",
"assets", "assets",
"locations", # "location",
"quantities", "quantity",
"is_group_assignment", "is_group_assignment",
"owners", "owners",
] ]
@ -119,25 +196,21 @@ class LogMasterView(WuttaFarmMasterView):
filter_defaults = { filter_defaults = {
"message": {"active": True, "verb": "contains"}, "message": {"active": True, "verb": "contains"},
"status": {"active": True, "verb": "not_equal", "value": "abandoned"},
} }
form_fields = [ form_fields = [
"message", "message",
"timestamp", "timestamp",
"assets", "assets",
"groups", "location",
"locations", "quantity",
"quantities",
"notes", "notes",
"status", "status",
"log_type", "log_type",
"owners", "owners",
"is_movement",
"is_group_assignment", "is_group_assignment",
"quick",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
def get_query(self, session=None): def get_query(self, session=None):
@ -145,26 +218,16 @@ class LogMasterView(WuttaFarmMasterView):
model = self.app.model model = self.app.model
model_class = self.get_model_class() model_class = self.get_model_class()
session = session or self.Session() session = session or self.Session()
query = session.query(model_class) return session.query(model_class).join(model.Log)
if model_class is not model.Log:
query = query.join(model.Log)
return query
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
super().configure_grid(g) super().configure_grid(g)
model = self.app.model model = self.app.model
enum = self.app.enum
# status # status
g.set_enum("status", enum.LOG_STATUS)
g.set_sorter("status", model.Log.status) g.set_sorter("status", model.Log.status)
g.set_filter( g.set_filter("status", model.Log.status)
"status",
model.Log.status,
verbs=["equal", "not_equal"],
choices=enum.LOG_STATUS,
)
# drupal_id # drupal_id
g.set_label("drupal_id", "ID", column_only=True) g.set_label("drupal_id", "ID", column_only=True)
@ -185,68 +248,13 @@ class LogMasterView(WuttaFarmMasterView):
# assets # assets
g.set_renderer("assets", self.render_assets_for_grid) 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): def render_assets_for_grid(self, log, field, value):
assets = getattr(log, field) return ", ".join([a.asset.asset_name for a in log.log._assets])
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
def configure_form(self, form): def configure_form(self, form):
f = form f = form
super().configure_form(f) super().configure_form(f)
enum = self.app.enum enum = self.app.enum
session = self.Session()
log = f.model_instance log = f.model_instance
# timestamp # timestamp
@ -259,25 +267,12 @@ class LogMasterView(WuttaFarmMasterView):
if self.creating or self.editing: if self.creating or self.editing:
f.remove("assets") # TODO: need to support this f.remove("assets") # TODO: need to support this
else: else:
f.set_node("assets", AssetRefs(self.request)) f.set_node("assets", LogAssetRefs(self.request))
# nb. must explicity declare value for non-standard field f.set_default("assets", [a.asset_uuid for a in log.log._assets])
f.set_default("assets", log.assets)
# groups # location
if self.creating or self.editing: if self.creating or self.editing:
f.remove("groups") # TODO: need to support this f.remove("location") # 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)
# log_type # log_type
if self.creating: if self.creating:
@ -285,19 +280,13 @@ class LogMasterView(WuttaFarmMasterView):
else: else:
f.set_node( f.set_node(
"log_type", "log_type",
WuttaDictEnum( WuttaDictEnum(self.request, get_log_type_enum(self.config)),
self.request, get_log_type_enum(self.config, session=session)
),
) )
f.set_readonly("log_type") f.set_readonly("log_type")
# quantities # quantity
if self.creating or self.editing: if self.creating or self.editing:
f.remove("quantities") # TODO: need to support this f.remove("quantity") # 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)
# notes # notes
f.set_widget("notes", "notes") f.set_widget("notes", "notes")
@ -305,23 +294,13 @@ class LogMasterView(WuttaFarmMasterView):
# owners # owners
if self.creating or self.editing: if self.creating or self.editing:
f.remove("owners") # TODO: need to support this 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 # status
f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS)) f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS))
# is_movement
f.set_node("is_movement", colander.Boolean())
# is_group_assignment # is_group_assignment
f.set_node("is_group_assignment", colander.Boolean()) f.set_node("is_group_assignment", colander.Boolean())
# quick
f.set_readonly("quick") # TODO
def objectify(self, form): def objectify(self, form):
log = super().objectify(form) log = super().objectify(form)
@ -354,68 +333,6 @@ class LogMasterView(WuttaFarmMasterView):
return buttons 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): def defaults(config, **kwargs):
base = globals() base = globals()
@ -423,8 +340,8 @@ def defaults(config, **kwargs):
LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"]) LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"])
LogTypeView.defaults(config) LogTypeView.defaults(config)
AllLogView = kwargs.get("AllLogView", base["AllLogView"]) LogView = kwargs.get("LogView", base["LogView"])
AllLogView.defaults(config) LogView.defaults(config)
def includeme(config): def includeme(config):

View file

@ -36,7 +36,6 @@ class ActivityLogView(LogMasterView):
route_prefix = "logs_activity" route_prefix = "logs_activity"
url_prefix = "/logs/activity" url_prefix = "/logs/activity"
farmos_bundle = "activity"
farmos_refurl_path = "/logs/activity" farmos_refurl_path = "/logs/activity"

View file

@ -36,19 +36,8 @@ class HarvestLogView(LogMasterView):
route_prefix = "logs_harvest" route_prefix = "logs_harvest"
url_prefix = "/logs/harvest" url_prefix = "/logs/harvest"
farmos_bundle = "harvest"
farmos_refurl_path = "/logs/harvest" farmos_refurl_path = "/logs/harvest"
grid_columns = [
"status",
"drupal_id",
"timestamp",
"message",
"assets",
"quantities",
"owners",
]
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -36,29 +36,8 @@ class MedicalLogView(LogMasterView):
route_prefix = "logs_medical" route_prefix = "logs_medical"
url_prefix = "/logs/medical" url_prefix = "/logs/medical"
farmos_bundle = "medical"
farmos_refurl_path = "/logs/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): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -36,21 +36,8 @@ class ObservationLogView(LogMasterView):
route_prefix = "logs_observation" route_prefix = "logs_observation"
url_prefix = "/logs/observation" url_prefix = "/logs/observation"
farmos_bundle = "observation"
farmos_refurl_path = "/logs/observation" farmos_refurl_path = "/logs/observation"
grid_columns = [
"status",
"drupal_id",
"timestamp",
"message",
"assets",
"locations",
"groups",
"is_group_assignment",
"owners",
]
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -27,7 +27,7 @@ from webhelpers2.html import tags
from wuttaweb.views import MasterView 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): class WuttaFarmMasterView(MasterView):
@ -36,8 +36,6 @@ class WuttaFarmMasterView(MasterView):
""" """
farmos_refurl_path = None farmos_refurl_path = None
farmos_entity_type = None
farmos_bundle = None
labels = { labels = {
"farmos_uuid": "farmOS UUID", "farmos_uuid": "farmOS UUID",
@ -106,42 +104,5 @@ class WuttaFarmMasterView(MasterView):
f.set_readonly("drupal_id") f.set_readonly("drupal_id")
def persist(self, obj, session=None): def persist(self, obj, session=None):
# save per usual
super().persist(obj, session) super().persist(obj, session)
self.app.auto_sync_to_farmos(obj, require=False)
# 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)

View file

@ -23,16 +23,12 @@
Master view for Plants Master view for Plants
""" """
from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.util import get_form_data
from wuttafarm.db.model import PlantType, PlantAsset from wuttafarm.db.model import PlantType, PlantAsset
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
from wuttafarm.web.forms.schema import PlantTypeRefs from wuttafarm.web.forms.schema import PlantTypeRefs
from wuttafarm.web.forms.widgets import ImageWidget from wuttafarm.web.forms.widgets import ImageWidget
from wuttafarm.web.util import get_farmos_client_for_user
class PlantTypeView(AssetTypeMasterView): class PlantTypeView(AssetTypeMasterView):
@ -44,8 +40,6 @@ class PlantTypeView(AssetTypeMasterView):
route_prefix = "plant_types" route_prefix = "plant_types"
url_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" farmos_refurl_path = "/admin/structure/taxonomy/manage/plant_type/overview"
grid_columns = [ grid_columns = [
@ -62,8 +56,8 @@ class PlantTypeView(AssetTypeMasterView):
form_fields = [ form_fields = [
"name", "name",
"description", "description",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
has_rows = True has_rows = True
@ -104,19 +98,6 @@ class PlantTypeView(AssetTypeMasterView):
return buttons 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): def get_row_grid_data(self, plant_type):
model = self.app.model model = self.app.model
session = self.Session() session = self.Session()
@ -145,55 +126,6 @@ class PlantTypeView(AssetTypeMasterView):
def get_row_action_url_view(self, plant, i): def get_row_action_url_view(self, plant, i):
return self.request.route_url("plant_assets.view", uuid=plant.uuid) 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): class PlantAssetView(AssetMasterView):
""" """
@ -204,7 +136,6 @@ class PlantAssetView(AssetMasterView):
route_prefix = "plant_assets" route_prefix = "plant_assets"
url_prefix = "/assets/plant" url_prefix = "/assets/plant"
farmos_bundle = "plant"
farmos_refurl_path = "/assets/plant" farmos_refurl_path = "/assets/plant"
labels = { labels = {
@ -227,8 +158,8 @@ class PlantAssetView(AssetMasterView):
"notes", "notes",
"asset_type", "asset_type",
"archived", "archived",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
"thumbnail_url", "thumbnail_url",
"image_url", "image_url",
"thumbnail", "thumbnail",
@ -240,20 +171,10 @@ class PlantAssetView(AssetMasterView):
super().configure_grid(g) super().configure_grid(g)
# plant_types # 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): def render_grid_plant_types(self, plant, field, value):
plant_types = plant._plant_types return ", ".join([t.plant_type.name for t in 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 configure_form(self, form): def configure_form(self, form):
f = form f = form
@ -262,38 +183,18 @@ class PlantAssetView(AssetMasterView):
plant = f.model_instance plant = f.model_instance
# plant_types # 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_node("plant_types", PlantTypeRefs(self.request))
if not self.creating: f.set_default(
# nb. must explcitly declare value for non-standard field "plant_types", [t.plant_type_uuid for t in plant._plant_types]
f.set_default("plant_types", [pt.uuid for pt in plant.plant_types]) )
# season # season
if self.creating or self.editing: if self.creating or self.editing:
f.remove("season") # TODO: add support for this 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): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -29,7 +29,7 @@ from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
from wuttafarm.web.forms.schema import UnitRef, LogRef from wuttafarm.web.forms.schema import UnitRef
def get_quantity_type_enum(config): def get_quantity_type_enum(config):
@ -66,8 +66,8 @@ class QuantityTypeView(WuttaFarmMasterView):
form_fields = [ form_fields = [
"name", "name",
"description", "description",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
def configure_grid(self, grid): def configure_grid(self, grid):
@ -119,9 +119,8 @@ class QuantityMasterView(WuttaFarmMasterView):
"value", "value",
"units", "units",
"label", "label",
"log",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
def get_query(self, session=None): def get_query(self, session=None):
@ -232,13 +231,6 @@ class QuantityMasterView(WuttaFarmMasterView):
# TODO: ugh # TODO: ugh
f.set_default("units", quantity.quantity.units) f.set_default("units", quantity.quantity.units)
# log
if self.creating or self.editing:
f.remove("log")
else:
f.set_node("log", LogRef(self.request))
f.set_default("log", quantity.log)
def get_xref_buttons(self, quantity): def get_xref_buttons(self, quantity):
buttons = super().get_xref_buttons(quantity) buttons = super().get_xref_buttons(quantity)
@ -256,7 +248,7 @@ class QuantityMasterView(WuttaFarmMasterView):
return buttons return buttons
class AllQuantityView(QuantityMasterView): class QuantityView(QuantityMasterView):
""" """
Master view for All Quantities Master view for All Quantities
""" """
@ -288,8 +280,8 @@ def defaults(config, **kwargs):
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"]) QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
QuantityTypeView.defaults(config) QuantityTypeView.defaults(config)
AllQuantityView = kwargs.get("AllQuantityView", base["AllQuantityView"]) QuantityView = kwargs.get("QuantityView", base["QuantityView"])
AllQuantityView.defaults(config) QuantityView.defaults(config)
StandardQuantityView = kwargs.get( StandardQuantityView = kwargs.get(
"StandardQuantityView", base["StandardQuantityView"] "StandardQuantityView", base["StandardQuantityView"]

View file

@ -27,9 +27,4 @@ from .base import QuickFormView
def includeme(config): 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") config.include("wuttafarm.web.views.quick.eggs")

View file

@ -28,9 +28,8 @@ import logging
from pyramid.renderers import render_to_response from pyramid.renderers import render_to_response
from wuttaweb.views import View from wuttaweb.views import View
from wuttaweb.db import Session
from wuttafarm.web.util import get_farmos_client_for_user from wuttafarm.web.util import save_farmos_oauth2_token
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -41,11 +40,9 @@ class QuickFormView(View):
Base class for quick form views. Base class for quick form views.
""" """
Session = Session
def __init__(self, request, context=None): def __init__(self, request, context=None):
super().__init__(request, context=context) super().__init__(request, context=context)
self.farmos_client = get_farmos_client_for_user(self.request) self.farmos_client = self.get_farmos_client()
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client) self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
self.normal = self.app.get_normalizer(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): def get_template_context(self, context):
return 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 @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._defaults(config) cls._defaults(config)
@ -138,10 +151,6 @@ class QuickFormView(View):
def _defaults(cls, config): def _defaults(cls, config):
route_slug = cls.get_route_slug() route_slug = cls.get_route_slug()
url_slug = cls.get_url_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_route(f"quick.{route_slug}", f"/quick/{url_slug}")
config.add_view( config.add_view(cls, route_name=f"quick.{route_slug}")
cls, route_name=f"quick.{route_slug}", permission=f"quick.{route_slug}"
)

View file

@ -48,9 +48,6 @@ class EggsQuickForm(QuickFormView):
_layer_assets = None _layer_assets = None
# TODO: make this configurable?
unit_name = "egg(s)"
def make_quick_form(self): def make_quick_form(self):
f = self.make_form( f = self.make_form(
fields=[ fields=[
@ -91,47 +88,6 @@ class EggsQuickForm(QuickFormView):
if self._layer_assets is not None: if self._layer_assets is not None:
return self._layer_assets return self._layer_assets
if self.app.is_farmos_wrapper():
assets = self.get_layer_assets_from_farmos()
else:
assets = self.get_layer_assets_from_wuttafarm()
assets.sort(key=lambda a: a["name"])
self._layer_assets = assets
return assets
def get_layer_assets_from_wuttafarm(self):
model = self.app.model
session = self.Session()
assets = []
def normalize(asset):
asset_type = asset.__wutta_hint__["farmos_asset_type"]
return {
"uuid": str(asset.farmos_uuid),
"name": asset.asset_name,
"type": f"asset--{asset_type}",
}
query = (
session.query(model.AnimalAsset)
.join(model.Asset)
.filter(model.AnimalAsset.produces_eggs == True)
.order_by(model.Asset.asset_name)
)
assets.extend([normalize(a) for a in query])
query = (
session.query(model.GroupAsset)
.join(model.Asset)
.filter(model.GroupAsset.produces_eggs == True)
.order_by(model.Asset.asset_name)
)
assets.extend([normalize(a) for a in query])
return assets
def get_layer_assets_from_farmos(self):
assets = [] assets = []
params = { params = {
"filter[produces_eggs]": 1, "filter[produces_eggs]": 1,
@ -151,14 +107,21 @@ class EggsQuickForm(QuickFormView):
result = self.farmos_client.asset.get("group", params=params) result = self.farmos_client.asset.get("group", params=params)
assets.extend([normalize(a) for a in result["data"]]) assets.extend([normalize(a) for a in result["data"]])
assets.sort(key=lambda a: a["name"])
self._layer_assets = assets
return assets return assets
def save_quick_form(self, form): def save_quick_form(self, form):
if self.app.is_farmos_wrapper(): response = self.save_to_farmos(form)
return 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): def save_to_farmos(self, form):
data = form.validated data = form.validated
@ -168,7 +131,7 @@ class EggsQuickForm(QuickFormView):
asset = assets[data["asset"]] asset = assets[data["asset"]]
# TODO: make this configurable? # TODO: make this configurable?
unit_name = self.unit_name unit_name = "egg(s)"
unit = {"data": {"type": "taxonomy_term--unit"}} unit = {"data": {"type": "taxonomy_term--unit"}}
new_unit = None new_unit = None
@ -229,7 +192,6 @@ class EggsQuickForm(QuickFormView):
"type": "log--harvest", "type": "log--harvest",
"attributes": { "attributes": {
"name": f"Collected {data['count']} {unit_name}", "name": f"Collected {data['count']} {unit_name}",
"timestamp": self.app.localtime(data["timestamp"]).timestamp(),
"notes": notes, "notes": notes,
"quick": ["eggs"], "quick": ["eggs"],
}, },
@ -267,87 +229,13 @@ class EggsQuickForm(QuickFormView):
blueprints.insert(0, new_unit) blueprints.insert(0, new_unit)
blueprint = SubrequestsBlueprint.parse_obj(blueprints) blueprint = SubrequestsBlueprint.parse_obj(blueprints)
response = self.farmos_client.subrequests.send(blueprint, format=Format.json) response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
return response
log = json.loads(response["create-log#body{0}"]["body"]) def redirect_after_save(self, result):
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( return self.redirect(
self.request.route_url("logs_harvest.view", uuid=log.uuid) self.request.route_url(
"farmos_logs_harvest.view", uuid=result["data"]["id"]
) )
return self.redirect(
self.request.route_url("farmos_logs_harvest.view", uuid=log["data"]["id"])
) )

View file

@ -57,21 +57,10 @@ class AppInfoView(base.AppInfoView):
return info return info
def configure_get_simple_settings(self): # pylint: disable=empty-docstring 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 = super().configure_get_simple_settings()
simple_settings.extend( simple_settings.extend(
[ [
{ {"name": "farmos.url.base"},
"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": f"{self.app.appname}.farmos_integration_mode", "name": f"{self.app.appname}.farmos_integration_mode",
"default": self.app.get_farmos_integration_mode(), "default": self.app.get_farmos_integration_mode(),

View file

@ -50,8 +50,8 @@ class StructureTypeView(AssetTypeMasterView):
form_fields = [ form_fields = [
"name", "name",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
] ]
has_rows = True has_rows = True
@ -128,19 +128,6 @@ class StructureTypeView(AssetTypeMasterView):
def get_row_action_url_view(self, structure, i): def get_row_action_url_view(self, structure, i):
return self.request.route_url("structure_assets.view", uuid=structure.uuid) 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): class StructureAssetView(AssetMasterView):
""" """
@ -151,7 +138,6 @@ class StructureAssetView(AssetMasterView):
route_prefix = "structure_assets" route_prefix = "structure_assets"
url_prefix = "/asset/structures" url_prefix = "/asset/structures"
farmos_bundle = "structure"
farmos_refurl_path = "/assets/structure" farmos_refurl_path = "/assets/structure"
grid_columns = [ grid_columns = [
@ -160,7 +146,6 @@ class StructureAssetView(AssetMasterView):
"asset_name", "asset_name",
"structure_type", "structure_type",
"parents", "parents",
"owners",
"archived", "archived",
] ]
@ -173,8 +158,8 @@ class StructureAssetView(AssetMasterView):
"is_location", "is_location",
"is_fixed", "is_fixed",
"archived", "archived",
"drupal_id",
"farmos_uuid", "farmos_uuid",
"drupal_id",
"thumbnail_url", "thumbnail_url",
"image_url", "image_url",
"thumbnail", "thumbnail",

View file

@ -24,7 +24,7 @@ Master view for Units
""" """
from wuttafarm.web.views import WuttaFarmMasterView from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import Measure, Unit, Quantity from wuttafarm.db.model import Measure, Unit
class MeasureView(WuttaFarmMasterView): class MeasureView(WuttaFarmMasterView):
@ -52,26 +52,6 @@ class MeasureView(WuttaFarmMasterView):
"drupal_id", "drupal_id",
] ]
has_rows = True
row_model_class = Quantity
rows_viewable = True
row_labels = {
"quantity_type_id": "Quantity Type ID",
"measure_id": "Measure ID",
}
row_grid_columns = [
"drupal_id",
"as_text",
"quantity_type",
"value",
"units",
"label",
]
rows_sort_defaults = ("drupal_id", "desc")
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
super().configure_grid(g) super().configure_grid(g)
@ -79,50 +59,6 @@ class MeasureView(WuttaFarmMasterView):
# name # name
g.set_link("name") g.set_link("name")
def get_row_grid_data(self, measure):
model = self.app.model
session = self.Session()
return session.query(model.Quantity).filter(model.Quantity.measure == measure)
def configure_row_grid(self, grid):
g = grid
super().configure_row_grid(g)
# drupal_id
g.set_label("drupal_id", "ID", column_only=True)
# as_text
g.set_renderer("as_text", self.render_as_text_for_grid)
g.set_link("as_text")
# value
g.set_renderer("value", self.render_value_for_grid)
def render_as_text_for_grid(self, quantity, field, value):
return quantity.render_as_text(self.config)
def render_value_for_grid(self, quantity, field, value):
value = quantity.value_numerator / quantity.value_denominator
return self.app.render_quantity(value)
def get_row_action_url_view(self, quantity, i):
return self.request.route_url(
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
)
@classmethod
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): class UnitView(WuttaFarmMasterView):
""" """
@ -133,8 +69,6 @@ class UnitView(WuttaFarmMasterView):
route_prefix = "units" route_prefix = "units"
url_prefix = "/units" url_prefix = "/units"
farmos_entity_type = "taxonomy_term"
farmos_bundle = "unit"
farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview" farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
grid_columns = [ grid_columns = [
@ -151,30 +85,10 @@ class UnitView(WuttaFarmMasterView):
form_fields = [ form_fields = [
"name", "name",
"description", "description",
"drupal_id",
"farmos_uuid", "farmos_uuid",
]
has_rows = True
row_model_class = Quantity
rows_viewable = True
row_labels = {
"quantity_type_id": "Quantity Type ID",
"measure_id": "Measure ID",
}
row_grid_columns = [
"drupal_id", "drupal_id",
"as_text",
"quantity_type",
"measure",
"value",
"label",
] ]
rows_sort_defaults = ("drupal_id", "desc")
def configure_grid(self, grid): def configure_grid(self, grid):
g = grid g = grid
super().configure_grid(g) super().configure_grid(g)
@ -202,37 +116,6 @@ class UnitView(WuttaFarmMasterView):
return buttons return buttons
def get_row_grid_data(self, unit):
model = self.app.model
session = self.Session()
return session.query(model.Quantity).filter(model.Quantity.units == unit)
def configure_row_grid(self, grid):
g = grid
super().configure_row_grid(g)
# drupal_id
g.set_label("drupal_id", "ID", column_only=True)
# as_text
g.set_renderer("as_text", self.render_as_text_for_grid)
g.set_link("as_text")
# value
g.set_renderer("value", self.render_value_for_grid)
def render_as_text_for_grid(self, quantity, field, value):
return quantity.render_as_text(self.config)
def render_value_for_grid(self, quantity, field, value):
value = quantity.value_numerator / quantity.value_denominator
return self.app.render_quantity(value)
def get_row_action_url_view(self, quantity, i):
return self.request.route_url(
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
)
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -55,13 +55,11 @@ class UserView(base.UserView):
# farmos_uuid # farmos_uuid
if not self.creating: if not self.creating:
f.fields.append("farmos_uuid") f.fields.append("farmos_uuid")
f.set_readonly("farmos_uuid")
f.set_default("farmos_uuid", user.farmos_uuid or colander.null) f.set_default("farmos_uuid", user.farmos_uuid or colander.null)
# drupal_id # drupal_id
if not self.creating: if not self.creating:
f.fields.append("drupal_id") f.fields.append("drupal_id")
f.set_readonly("drupal_id")
f.set_default("drupal_id", user.drupal_id or colander.null) f.set_default("drupal_id", user.drupal_id or colander.null)
def get_xref_buttons(self, user): def get_xref_buttons(self, user):