Compare commits
55 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af2ea18e1d | |||
| 23af35842d | |||
| 609a900f39 | |||
| a547188a90 | |||
| 81fa22bbd8 | |||
| b2b49d93ae | |||
| 7bffa6cba6 | |||
| 0a1aee591a | |||
| a0f73e6a32 | |||
| e8a8ce2528 | |||
| b2c3d3a301 | |||
| 759eb906b9 | |||
| 41870ee2e2 | |||
| 0ac2485bff | |||
| eb16990b0b | |||
| ce103137a5 | |||
| 547cc6e4ae | |||
| 32d23a7073 | |||
| 7890b18568 | |||
| 90ff7eb793 | |||
| d07f3ed716 | |||
| 7d2ae48067 | |||
| 1d877545ae | |||
| 87f3764ebf | |||
| 3ae4d639ec | |||
| a5550091d3 | |||
| 61402c183e | |||
| 64e4392a92 | |||
| ae73d2f87f | |||
| 86e36bc64a | |||
| d1817a3611 | |||
| d465934818 | |||
| c353d5bcef | |||
| bdda586ccd | |||
| 0d989dcb2c | |||
| 2f84f76d89 | |||
| 3343524325 | |||
| 28ecb4d786 | |||
| 338da0208c | |||
| ec67340e66 | |||
| 1c0286eda0 | |||
| 7d5ff47e8e | |||
| 5046171b76 | |||
| f374ae426c | |||
| 2a375b0a6f | |||
| a5d7f89fcb | |||
| 96ccf30e46 | |||
| 38dad49bbd | |||
| f2be7d0a53 | |||
| 9b4afb845b | |||
| f4b5f3960c | |||
| 127ea49d74 | |||
| 30e1fd23d6 | |||
| df517cfbfa | |||
| ec6ac443fb |
70 changed files with 3762 additions and 519 deletions
68
CHANGELOG.md
68
CHANGELOG.md
|
|
@ -5,6 +5,74 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaFarm"
|
name = "WuttaFarm"
|
||||||
version = "0.6.0"
|
version = "0.8.0"
|
||||||
description = "Web app to integrate with and extend farmOS"
|
description = "Web app to integrate with and extend farmOS"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|
@ -34,7 +34,7 @@ dependencies = [
|
||||||
"pyramid_exclog",
|
"pyramid_exclog",
|
||||||
"uvicorn[standard]",
|
"uvicorn[standard]",
|
||||||
"WuttaSync",
|
"WuttaSync",
|
||||||
"WuttaWeb[continuum]>=0.28.1",
|
"WuttaWeb[continuum]>=0.29.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,21 @@ 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.
|
||||||
|
|
@ -136,7 +151,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, require=True):
|
def auto_sync_to_farmos(self, obj, model_name=None, client=None, require=True):
|
||||||
"""
|
"""
|
||||||
Export the given object to farmOS, using configured handler.
|
Export the given object to farmOS, using configured handler.
|
||||||
|
|
||||||
|
|
@ -147,6 +162,9 @@ 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
|
||||||
|
|
@ -162,14 +180,12 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
# nb. begin txn to establish the API client
|
# nb. begin txn to establish the API client
|
||||||
# TODO: should probably use current user oauth2 token instead
|
handler.begin_target_transaction(client)
|
||||||
# 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, require=True):
|
def auto_sync_from_farmos(self, obj, model_name, client=None, require=True):
|
||||||
"""
|
"""
|
||||||
Import the given object from farmOS, using configured handler.
|
Import the given object from farmOS, using configured handler.
|
||||||
|
|
||||||
|
|
@ -178,6 +194,9 @@ 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
|
||||||
|
|
@ -191,9 +210,7 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
# nb. begin txn to establish the API client
|
# nb. begin txn to establish the API client
|
||||||
# TODO: should probably use current user oauth2 token instead
|
handler.begin_source_transaction(client)
|
||||||
# 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)
|
||||||
|
|
|
||||||
65
src/wuttafarm/assets.py
Normal file
65
src/wuttafarm/assets.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# -*- 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 []
|
||||||
|
|
@ -50,11 +50,12 @@ class WuttaFarmConfig(WuttaConfigExtension):
|
||||||
f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler"
|
f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
# web app menu
|
# web app stuff
|
||||||
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')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""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,
|
||||||
|
)
|
||||||
108
src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py
Normal file
108
src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""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,
|
||||||
|
)
|
||||||
111
src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py
Normal file
111
src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""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")
|
||||||
|
|
@ -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
|
from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -186,6 +187,25 @@ 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 ""
|
||||||
|
|
||||||
|
|
@ -196,7 +216,12 @@ class AssetMixin:
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def asset(cls):
|
def asset(cls):
|
||||||
return orm.relationship(Asset)
|
return orm.relationship(
|
||||||
|
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 ""
|
||||||
|
|
@ -213,6 +238,8 @@ 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:
|
||||||
|
|
@ -250,3 +277,27 @@ 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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ 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.
|
||||||
""",
|
""",
|
||||||
|
|
@ -80,6 +79,14 @@ 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 ""
|
||||||
|
|
||||||
|
|
@ -103,6 +110,7 @@ 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(
|
||||||
|
|
|
||||||
|
|
@ -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": "animal",
|
"farmos_asset_type": "land",
|
||||||
}
|
}
|
||||||
|
|
||||||
land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False, unique=True)
|
land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False)
|
||||||
land_type = orm.relationship(LandType, back_populates="land_assets")
|
land_type = orm.relationship(LandType, back_populates="land_assets")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -80,6 +81,12 @@ 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 ""
|
||||||
|
|
||||||
|
|
@ -99,9 +106,17 @@ 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)
|
||||||
|
|
||||||
|
|
@ -129,4 +144,5 @@ class PlantAssetPlantType(model.Base):
|
||||||
doc="""
|
doc="""
|
||||||
Reference to the plant type.
|
Reference to the plant type.
|
||||||
""",
|
""",
|
||||||
|
back_populates="_plant_assets",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -119,6 +120,22 @@ 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,
|
||||||
|
|
@ -135,6 +152,15 @@ 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,
|
||||||
|
|
@ -153,7 +179,70 @@ class Log(model.Base):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
_assets = orm.relationship("LogAsset", back_populates="log")
|
_assets = orm.relationship(
|
||||||
|
"LogAsset",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="log",
|
||||||
|
)
|
||||||
|
|
||||||
|
assets = association_proxy(
|
||||||
|
"_assets",
|
||||||
|
"asset",
|
||||||
|
creator=lambda asset: LogAsset(asset=asset),
|
||||||
|
)
|
||||||
|
|
||||||
|
_groups = orm.relationship(
|
||||||
|
"LogGroup",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="log",
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = association_proxy(
|
||||||
|
"_groups",
|
||||||
|
"asset",
|
||||||
|
creator=lambda asset: LogGroup(asset=asset),
|
||||||
|
)
|
||||||
|
|
||||||
|
_locations = orm.relationship(
|
||||||
|
"LogLocation",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="log",
|
||||||
|
)
|
||||||
|
|
||||||
|
locations = association_proxy(
|
||||||
|
"_locations",
|
||||||
|
"asset",
|
||||||
|
creator=lambda asset: LogLocation(asset=asset),
|
||||||
|
)
|
||||||
|
|
||||||
|
_quantities = orm.relationship(
|
||||||
|
"LogQuantity",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="log",
|
||||||
|
)
|
||||||
|
|
||||||
|
quantities = association_proxy(
|
||||||
|
"_quantities",
|
||||||
|
"quantity",
|
||||||
|
creator=lambda quantity: LogQuantity(quantity=quantity),
|
||||||
|
)
|
||||||
|
|
||||||
|
_owners = orm.relationship(
|
||||||
|
"LogOwner",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="log",
|
||||||
|
)
|
||||||
|
|
||||||
|
owners = association_proxy(
|
||||||
|
"_owners",
|
||||||
|
"user",
|
||||||
|
creator=lambda user: LogOwner(user=user),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.message or ""
|
return self.message or ""
|
||||||
|
|
@ -165,7 +254,12 @@ class LogMixin:
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def log(cls):
|
def log(cls):
|
||||||
return orm.relationship(Log)
|
return orm.relationship(
|
||||||
|
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 ""
|
||||||
|
|
@ -177,8 +271,16 @@ 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):
|
||||||
|
|
@ -203,3 +305,100 @@ 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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
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
|
||||||
|
|
@ -41,5 +43,13 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ Model definition for Quantities
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
|
||||||
from wuttjamaican.db import model
|
from wuttjamaican.db import model
|
||||||
|
|
||||||
|
|
@ -161,6 +162,25 @@ class Quantity(model.Base):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_log = orm.relationship(
|
||||||
|
"LogQuantity",
|
||||||
|
uselist=False,
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="quantity",
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_log_quantity(log):
|
||||||
|
from wuttafarm.db.model import LogQuantity
|
||||||
|
|
||||||
|
return LogQuantity(log=log)
|
||||||
|
|
||||||
|
log = association_proxy(
|
||||||
|
"_log",
|
||||||
|
"log",
|
||||||
|
creator=make_log_quantity,
|
||||||
|
)
|
||||||
|
|
||||||
def render_as_text(self, config=None):
|
def render_as_text(self, config=None):
|
||||||
measure = str(self.measure or self.measure_id or "")
|
measure = str(self.measure or self.measure_id or "")
|
||||||
value = self.value_numerator / self.value_denominator
|
value = self.value_numerator / self.value_denominator
|
||||||
|
|
@ -202,6 +222,7 @@ def add_quantity_proxies(subclass):
|
||||||
Quantity.make_proxy(subclass, "quantity", "units_uuid")
|
Quantity.make_proxy(subclass, "quantity", "units_uuid")
|
||||||
Quantity.make_proxy(subclass, "quantity", "units")
|
Quantity.make_proxy(subclass, "quantity", "units")
|
||||||
Quantity.make_proxy(subclass, "quantity", "label")
|
Quantity.make_proxy(subclass, "quantity", "label")
|
||||||
|
Quantity.make_proxy(subclass, "quantity", "log")
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantity(QuantityMixin, model.Base):
|
class StandardQuantity(QuantityMixin, model.Base):
|
||||||
|
|
|
||||||
|
|
@ -94,3 +94,9 @@ 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")
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,12 @@ 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"
|
||||||
|
|
@ -437,6 +443,138 @@ class StructureAssetImporter(ToFarmOSAsset):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# quantity importers
|
||||||
|
##############################
|
||||||
|
|
||||||
|
|
||||||
|
class ToFarmOSQuantity(ToFarmOS):
|
||||||
|
"""
|
||||||
|
Base class for quantity data importer targeting the farmOS API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
farmos_quantity_type = None
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"measure",
|
||||||
|
"value_numerator",
|
||||||
|
"value_denominator",
|
||||||
|
"label",
|
||||||
|
"quantity_type_uuid",
|
||||||
|
"unit_uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_target_objects(self, **kwargs):
|
||||||
|
return list(
|
||||||
|
self.farmos_client.resource.iterate("quantity", self.farmos_quantity_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
# fetch from cache, if applicable
|
||||||
|
if self.caches_target:
|
||||||
|
return super().get_target_object(key)
|
||||||
|
|
||||||
|
# okay now must fetch via API
|
||||||
|
if self.get_keys() != ["uuid"]:
|
||||||
|
raise ValueError("must use uuid key for this to work")
|
||||||
|
uuid = key[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
qty = self.farmos_client.resource.get_id(
|
||||||
|
"quantity", self.farmos_quantity_type, str(uuid)
|
||||||
|
)
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
return qty["data"]
|
||||||
|
|
||||||
|
def create_target_object(self, key, source_data):
|
||||||
|
if source_data.get("__ignoreme__"):
|
||||||
|
return None
|
||||||
|
if self.dry_run:
|
||||||
|
return source_data
|
||||||
|
|
||||||
|
payload = self.get_quantity_payload(source_data)
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"quantity", self.farmos_quantity_type, payload
|
||||||
|
)
|
||||||
|
normal = self.normalize_target_object(result["data"])
|
||||||
|
normal["_new_object"] = result["data"]
|
||||||
|
return normal
|
||||||
|
|
||||||
|
def update_target_object(self, quantity, source_data, target_data=None):
|
||||||
|
if self.dry_run:
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
payload = self.get_quantity_payload(source_data)
|
||||||
|
payload["id"] = str(source_data["uuid"])
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"quantity", self.farmos_quantity_type, payload
|
||||||
|
)
|
||||||
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
|
def normalize_target_object(self, qty):
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"uuid": UUID(qty["id"]),
|
||||||
|
"measure": qty["attributes"]["measure"],
|
||||||
|
"value_numerator": qty["attributes"]["value"]["numerator"],
|
||||||
|
"value_denominator": qty["attributes"]["value"]["denominator"],
|
||||||
|
"label": qty["attributes"]["label"],
|
||||||
|
"quantity_type_uuid": UUID(
|
||||||
|
qty["relationships"]["quantity_type"]["data"]["id"]
|
||||||
|
),
|
||||||
|
"unit_uuid": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit := qty["relationships"]["units"]["data"]:
|
||||||
|
result["unit_uuid"] = UUID(unit["id"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_quantity_payload(self, source_data):
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if "measure" in self.fields:
|
||||||
|
attrs["measure"] = source_data["measure"]
|
||||||
|
if "value_numerator" in self.fields and "value_denominator" in self.fields:
|
||||||
|
attrs["value"] = {
|
||||||
|
"numerator": source_data["value_numerator"],
|
||||||
|
"denominator": source_data["value_denominator"],
|
||||||
|
}
|
||||||
|
if "label" in self.fields:
|
||||||
|
attrs["label"] = source_data["label"]
|
||||||
|
|
||||||
|
rels = {}
|
||||||
|
if "quantity_type_uuid" in self.fields:
|
||||||
|
rels["quantity_type"] = {
|
||||||
|
"data": {
|
||||||
|
"id": str(source_data["quantity_type_uuid"]),
|
||||||
|
"type": "quantity_type--quantity_type",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if "unit_uuid" in self.fields:
|
||||||
|
rels["units"] = {
|
||||||
|
"data": {
|
||||||
|
"id": str(source_data["unit_uuid"]),
|
||||||
|
"type": "taxonomy_term--unit",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"attributes": attrs, "relationships": rels}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class StandardQuantityImporter(ToFarmOSQuantity):
|
||||||
|
|
||||||
|
model_title = "StandardQuantity"
|
||||||
|
farmos_quantity_type = "standard"
|
||||||
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# log importers
|
# log importers
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -452,9 +590,20 @@ 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"]
|
||||||
|
|
@ -500,14 +649,18 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
return self.normalize_target_object(result["data"])
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
def normalize_target_object(self, log):
|
def normalize_target_object(self, log):
|
||||||
|
normal = self.normal.normalize_farmos_log(log)
|
||||||
if notes := log["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": UUID(log["id"]),
|
"uuid": UUID(normal["uuid"]),
|
||||||
"name": log["attributes"]["name"],
|
"name": normal["name"],
|
||||||
"notes": notes,
|
"timestamp": self.app.make_utc(normal["timestamp"]),
|
||||||
|
"is_movement": normal["is_movement"],
|
||||||
|
"is_group_assignment": normal["is_group_assignment"],
|
||||||
|
"status": normal["status"],
|
||||||
|
"notes": normal["notes"],
|
||||||
|
"quick": normal["quick"],
|
||||||
|
"assets": [(a["asset_type"], UUID(a["uuid"])) for a in normal["assets"]],
|
||||||
|
"quantities": [UUID(uuid) for uuid in normal["quantity_uuids"]],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_log_payload(self, source_data):
|
def get_log_payload(self, source_data):
|
||||||
|
|
@ -515,11 +668,43 @@ 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"]
|
||||||
|
|
||||||
payload = {"attributes": attrs}
|
rels = {}
|
||||||
|
if "assets" in self.fields:
|
||||||
|
assets = []
|
||||||
|
for asset_type, uuid in source_data["assets"]:
|
||||||
|
assets.append(
|
||||||
|
{
|
||||||
|
"type": f"asset--{asset_type}",
|
||||||
|
"id": str(uuid),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rels["asset"] = {"data": assets}
|
||||||
|
if "quantities" in self.fields:
|
||||||
|
quantities = []
|
||||||
|
for uuid in source_data["quantities"]:
|
||||||
|
quantities.append(
|
||||||
|
{
|
||||||
|
# TODO: support other quantity types
|
||||||
|
"type": "quantity--standard",
|
||||||
|
"id": str(uuid),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rels["quantity"] = {"data": quantities}
|
||||||
|
|
||||||
|
payload = {"attributes": attrs, "relationships": rels}
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -540,6 +725,32 @@ 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):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,13 @@ 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):
|
def begin_target_transaction(self, client=None):
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
|
|
@ -98,8 +101,10 @@ 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
|
||||||
|
|
@ -263,6 +268,28 @@ 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
|
||||||
|
|
@ -321,6 +348,49 @@ class StructureAssetImporter(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# quantity importers
|
||||||
|
##############################
|
||||||
|
|
||||||
|
|
||||||
|
class FromWuttaFarmQuantity(FromWuttaFarm):
|
||||||
|
"""
|
||||||
|
Base class for WuttaFarm -> farmOS quantity importers
|
||||||
|
"""
|
||||||
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"measure",
|
||||||
|
"value_numerator",
|
||||||
|
"value_denominator",
|
||||||
|
"label",
|
||||||
|
"quantity_type_uuid",
|
||||||
|
"unit_uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize_source_object(self, qty):
|
||||||
|
return {
|
||||||
|
"uuid": qty.farmos_uuid or self.app.make_true_uuid(),
|
||||||
|
"measure": qty.measure_id,
|
||||||
|
"value_numerator": qty.value_numerator,
|
||||||
|
"value_denominator": qty.value_denominator,
|
||||||
|
"label": qty.label,
|
||||||
|
"quantity_type_uuid": qty.quantity_type.farmos_uuid,
|
||||||
|
"unit_uuid": qty.units.farmos_uuid,
|
||||||
|
"_src_object": qty,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StandardQuantityImporter(
|
||||||
|
FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
WuttaFarm → farmOS API exporter for Standard Quantities
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_model_class = model.StandardQuantity
|
||||||
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# log importers
|
# log importers
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -334,14 +404,28 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,6 +453,24 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,13 @@ class FromFarmOSHandler(ImportHandler):
|
||||||
source_key = "farmos"
|
source_key = "farmos"
|
||||||
generic_source_title = "farmOS"
|
generic_source_title = "farmOS"
|
||||||
|
|
||||||
def begin_source_transaction(self):
|
def begin_source_transaction(self, client=None):
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
|
|
@ -184,6 +187,7 @@ class AssetImporterBase(FromFarmOS, ToWutta):
|
||||||
fields.extend(
|
fields.extend(
|
||||||
[
|
[
|
||||||
"parents",
|
"parents",
|
||||||
|
"owners",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
@ -191,8 +195,9 @@ 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()
|
||||||
result = self.farmos_client.asset.get(asset_type)
|
return list(
|
||||||
return result["data"]
|
self.farmos_client.asset.iterate(asset_type, params={"include": "image"})
|
||||||
|
)
|
||||||
|
|
||||||
def normalize_source_data(self, **kwargs):
|
def normalize_source_data(self, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -205,10 +210,16 @@ class AssetImporterBase(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def normalize_asset(self, asset):
|
def normalize_source_object(self, asset):
|
||||||
""" """
|
""" """
|
||||||
image_url = None
|
data = self.normal.normalize_farmos_asset(asset)
|
||||||
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"):
|
||||||
|
|
@ -219,35 +230,20 @@ class AssetImporterBase(FromFarmOS, ToWutta):
|
||||||
if image_style := image["data"]["attributes"].get(
|
if image_style := image["data"]["attributes"].get(
|
||||||
"image_style_uri"
|
"image_style_uri"
|
||||||
):
|
):
|
||||||
image_url = image_style["large"]
|
data["image_url"] = image_style["large"]
|
||||||
thumbnail_url = image_style["thumbnail"]
|
data["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:
|
||||||
parents = []
|
data["parents"] = []
|
||||||
for parent in asset["relationships"]["parent"]["data"]:
|
for parent in asset["relationships"]["parent"]["data"]:
|
||||||
parents.append((self.get_asset_type(parent), UUID(parent["id"])))
|
data["parents"].append(
|
||||||
|
(self.get_asset_type(parent), UUID(parent["id"]))
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
if "owners" in self.fields:
|
||||||
"farmos_uuid": UUID(asset["id"]),
|
data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]]
|
||||||
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
|
||||||
"asset_name": asset["attributes"]["name"],
|
return data
|
||||||
"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]
|
||||||
|
|
@ -256,10 +252,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"] = [
|
data["parents"] = [(p.asset_type, p.farmos_uuid) for p in asset.parents]
|
||||||
(p.parent.asset_type, p.parent.farmos_uuid)
|
|
||||||
for p in asset.asset._parents
|
if "owners" in self.fields:
|
||||||
]
|
data["owners"] = [user.farmos_uuid for user in asset.owners]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -300,6 +296,30 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -335,11 +355,6 @@ 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
|
||||||
|
|
@ -371,10 +386,9 @@ class AnimalAssetImporter(AssetImporterBase):
|
||||||
else:
|
else:
|
||||||
sterile = animal["attributes"]["is_castrated"]
|
sterile = animal["attributes"]["is_castrated"]
|
||||||
|
|
||||||
data = self.normalize_asset(animal)
|
data = super().normalize_source_object(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,
|
||||||
|
|
@ -465,17 +479,11 @@ 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 = self.normalize_asset(group)
|
data = super().normalize_source_object(group)
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"asset_type": "group",
|
|
||||||
"produces_eggs": group["attributes"]["produces_eggs"],
|
"produces_eggs": group["attributes"]["produces_eggs"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -511,11 +519,6 @@ 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"]
|
||||||
|
|
@ -526,10 +529,9 @@ class LandAssetImporter(AssetImporterBase):
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = self.normalize_asset(land)
|
data = super().normalize_source_object(land)
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"asset_type": "land",
|
|
||||||
"land_type_uuid": land_type.uuid,
|
"land_type_uuid": land_type.uuid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -622,7 +624,7 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
def normalize_source_object(self, plant):
|
def normalize_source_object(self, plant):
|
||||||
""" """
|
""" """
|
||||||
plant_types = None
|
plant_types = []
|
||||||
if relationships := plant.get("relationships"):
|
if relationships := plant.get("relationships"):
|
||||||
|
|
||||||
if plant_type := relationships.get("plant_type"):
|
if plant_type := relationships.get("plant_type"):
|
||||||
|
|
@ -635,11 +637,10 @@ 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 = self.normalize_asset(plant)
|
data = super().normalize_source_object(plant)
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"asset_type": "plant",
|
"plant_types": set(plant_types),
|
||||||
"plant_types": plant_types,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
@ -648,7 +649,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"] = [t.plant_type_uuid for t in plant._plant_types]
|
data["plant_types"] = set([pt.uuid for pt in plant.plant_types])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -715,11 +716,6 @@ 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"]
|
||||||
|
|
@ -732,10 +728,9 @@ class StructureAssetImporter(AssetImporterBase):
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = self.normalize_asset(structure)
|
data = super().normalize_source_object(structure)
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"asset_type": "structure",
|
|
||||||
"structure_type_uuid": structure_type.uuid,
|
"structure_type_uuid": structure_type.uuid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -964,8 +959,11 @@ 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
|
||||||
|
|
@ -976,6 +974,10 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
fields.extend(
|
fields.extend(
|
||||||
[
|
[
|
||||||
"assets",
|
"assets",
|
||||||
|
"groups",
|
||||||
|
"locations",
|
||||||
|
"quantities",
|
||||||
|
"owners",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
@ -992,6 +994,7 @@ 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()
|
||||||
|
|
@ -1001,6 +1004,23 @@ 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):
|
||||||
|
|
@ -1008,9 +1028,25 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
if "assets" in self.fields:
|
if "assets" in self.fields:
|
||||||
data["assets"] = [
|
data["assets"] = [
|
||||||
(a.asset.asset_type, a.asset.farmos_uuid) for a in log.log._assets
|
(asset.asset_type, asset.farmos_uuid) for asset in 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):
|
||||||
|
|
@ -1023,14 +1059,13 @@ 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.log._assets.append(model.LogAsset(asset=asset))
|
log.assets.append(asset)
|
||||||
|
|
||||||
if target_data:
|
if target_data:
|
||||||
for key in target_data["assets"]:
|
for key in target_data["assets"]:
|
||||||
|
|
@ -1042,13 +1077,108 @@ 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.LogAsset)
|
self.target_session.query(model.Asset)
|
||||||
.filter(model.LogAsset.log == log)
|
.filter(model.Asset.asset_type == asset_type)
|
||||||
.filter(model.LogAsset.asset == asset)
|
.filter(model.Asset.farmos_uuid == farmos_uuid)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
self.target_session.delete(asset)
|
log.groups.append(asset)
|
||||||
|
|
||||||
|
if target_data:
|
||||||
|
for key in target_data["groups"]:
|
||||||
|
asset_type, farmos_uuid = key
|
||||||
|
if key not in source_data["groups"]:
|
||||||
|
asset = (
|
||||||
|
self.target_session.query(model.Asset)
|
||||||
|
.filter(model.Asset.asset_type == asset_type)
|
||||||
|
.filter(model.Asset.farmos_uuid == farmos_uuid)
|
||||||
|
.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
|
||||||
|
|
||||||
|
|
@ -1060,17 +1190,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1079,17 +1198,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1098,16 +1206,16 @@ class MedicalLogImporter(LogImporterBase):
|
||||||
|
|
||||||
model_class = model.MedicalLog
|
model_class = model.MedicalLog
|
||||||
|
|
||||||
supported_fields = [
|
def get_simple_fields(self):
|
||||||
"farmos_uuid",
|
""" """
|
||||||
"drupal_id",
|
fields = list(super().get_simple_fields())
|
||||||
"log_type",
|
# nb. must explicitly declare proxy fields
|
||||||
"message",
|
fields.extend(
|
||||||
"timestamp",
|
[
|
||||||
"notes",
|
"vet",
|
||||||
"status",
|
|
||||||
"assets",
|
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
class ObservationLogImporter(LogImporterBase):
|
class ObservationLogImporter(LogImporterBase):
|
||||||
|
|
@ -1117,17 +1225,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,40 @@ 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"]:
|
||||||
|
|
@ -96,8 +130,12 @@ 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"):
|
||||||
|
|
@ -132,6 +170,54 @@ 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"]
|
||||||
|
|
@ -188,12 +274,19 @@ 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"),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/wuttafarm/util.py
Normal file
37
src/wuttafarm/util.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- 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
|
||||||
|
|
@ -40,6 +40,15 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -55,6 +56,12 @@ 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):
|
||||||
|
|
||||||
|
|
@ -70,6 +77,31 @@ class LogQuick(WuttaSet):
|
||||||
return LogQuickWidget(**kwargs)
|
return LogQuickWidget(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LogRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Custom schema type for a
|
||||||
|
:class:`~wuttafarm.db.model.log.Log` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
return model.Log
|
||||||
|
|
||||||
|
def sort_query(self, query): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
return query.order_by(self.model_class.message)
|
||||||
|
|
||||||
|
def get_object_url(self, obj): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
log = obj
|
||||||
|
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
||||||
|
|
||||||
|
|
||||||
class FarmOSUnitRef(colander.SchemaType):
|
class FarmOSUnitRef(colander.SchemaType):
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
def serialize(self, node, appstruct):
|
||||||
|
|
@ -185,25 +217,6 @@ 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):
|
||||||
|
|
@ -255,13 +268,23 @@ class PlantTypeRefs(WuttaSet):
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
def serialize(self, node, appstruct):
|
||||||
if not appstruct:
|
if not appstruct:
|
||||||
appstruct = []
|
return colander.null
|
||||||
uuids = [u.hex for u in appstruct]
|
|
||||||
return json.dumps(uuids)
|
return [uuid.hex for uuid in appstruct]
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -366,21 +389,55 @@ class AssetParentRefs(WuttaSet):
|
||||||
return AssetParentRefsWidget(self.request, **kwargs)
|
return AssetParentRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LogAssetRefs(WuttaSet):
|
class AssetRefs(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:
|
||||||
appstruct = []
|
return colander.null
|
||||||
uuids = [u.hex for u in appstruct]
|
|
||||||
return json.dumps(uuids)
|
return {asset.uuid for asset in appstruct}
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
def widget_maker(self, **kwargs):
|
||||||
from wuttafarm.web.forms.widgets import LogAssetRefsWidget
|
from wuttafarm.web.forms.widgets import AssetRefsWidget
|
||||||
|
|
||||||
return LogAssetRefsWidget(self.request, **kwargs)
|
return AssetRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LogQuantityRefs(WuttaSet):
|
||||||
|
"""
|
||||||
|
Schema type for Quantities field (on a Log record)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, node, appstruct):
|
||||||
|
if not appstruct:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
return {qty.uuid for qty in appstruct}
|
||||||
|
|
||||||
|
def widget_maker(self, **kwargs):
|
||||||
|
from wuttafarm.web.forms.widgets import LogQuantityRefsWidget
|
||||||
|
|
||||||
|
return LogQuantityRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerRefs(WuttaSet):
|
||||||
|
"""
|
||||||
|
Schema type for Owners field (on a Log record)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, node, appstruct):
|
||||||
|
if not appstruct:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
return {user.uuid for user in appstruct}
|
||||||
|
|
||||||
|
def widget_maker(self, **kwargs):
|
||||||
|
from wuttafarm.web.forms.widgets import OwnerRefsWidget
|
||||||
|
|
||||||
|
return OwnerRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Notes(colander.String):
|
class Notes(colander.String):
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ Custom form widgets for WuttaFarm
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform.widget import Widget, SelectWidget
|
from deform.widget import Widget, SelectWidget, sequence_types, _normalize_choices
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
|
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget
|
||||||
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,33 +228,6 @@ 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.
|
||||||
|
|
@ -285,22 +258,40 @@ class FarmOSPlantTypesWidget(Widget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
class PlantTypeRefsWidget(WuttaCheckboxChoiceWidget):
|
class PlantTypeRefsWidget(Widget):
|
||||||
"""
|
"""
|
||||||
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()
|
||||||
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
if cstruct in (colander.null, None):
|
||||||
if readonly:
|
cstruct = ()
|
||||||
plant_types = []
|
|
||||||
for uuid in json.loads(cstruct):
|
if readonly := kw.get("readonly", self.readonly):
|
||||||
plant_type = session.get(model.PlantType, uuid)
|
items = []
|
||||||
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(
|
||||||
|
|
@ -311,9 +302,34 @@ class PlantTypeRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return HTML.tag("ul", c=plant_types)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
return HTML.tag("ul", c=items)
|
||||||
|
|
||||||
|
values = kw.get("values", self.values)
|
||||||
|
if not isinstance(values, sequence_types):
|
||||||
|
raise TypeError("Values must be a sequence type (list, tuple, or range).")
|
||||||
|
|
||||||
|
kw["values"] = _normalize_choices(values)
|
||||||
|
tmpl_values = self.get_template_values(field, cstruct, kw)
|
||||||
|
return field.renderer(self.template, **tmpl_values)
|
||||||
|
|
||||||
|
def get_template_values(self, field, cstruct, kw):
|
||||||
|
""" """
|
||||||
|
values = super().get_template_values(field, cstruct, kw)
|
||||||
|
|
||||||
|
values["js_values"] = json.dumps(values["values"])
|
||||||
|
|
||||||
|
if self.request.has_perm("plant_types.create"):
|
||||||
|
values["can_create"] = True
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def deserialize(self, field, pstruct):
|
||||||
|
""" """
|
||||||
|
if not pstruct:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
return set(pstruct.split(","))
|
||||||
|
|
||||||
|
|
||||||
class StructureWidget(Widget):
|
class StructureWidget(Widget):
|
||||||
|
|
@ -372,6 +388,11 @@ 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.
|
||||||
|
|
@ -403,9 +424,9 @@ class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
class LogAssetRefsWidget(WuttaCheckboxChoiceWidget):
|
class AssetRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
"""
|
"""
|
||||||
Widget for Assets field (on a Log record)
|
Widget for Assets field (of various kinds).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
def serialize(self, field, cstruct, **kw):
|
||||||
|
|
@ -416,7 +437,7 @@ class LogAssetRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
readonly = kw.get("readonly", self.readonly)
|
readonly = kw.get("readonly", self.readonly)
|
||||||
if readonly:
|
if readonly:
|
||||||
assets = []
|
assets = []
|
||||||
for uuid in json.loads(cstruct):
|
for uuid in cstruct or []:
|
||||||
asset = session.get(model.Asset, uuid)
|
asset = session.get(model.Asset, uuid)
|
||||||
assets.append(
|
assets.append(
|
||||||
HTML.tag(
|
HTML.tag(
|
||||||
|
|
@ -432,3 +453,85 @@ class LogAssetRefsWidget(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
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
{
|
{
|
||||||
"title": "Eggs",
|
"title": "Eggs",
|
||||||
"route": "quick.eggs",
|
"route": "quick.eggs",
|
||||||
# "perm": "assets.list",
|
"perm": "quick.eggs",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,30 @@
|
||||||
</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">
|
||||||
|
|
@ -22,6 +45,16 @@
|
||||||
<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"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<%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()}
|
||||||
|
|
@ -14,3 +15,8 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_vue_templates()">
|
||||||
|
${parent.render_vue_templates()}
|
||||||
|
${make_wuttafarm_components()}
|
||||||
|
</%def>
|
||||||
|
|
|
||||||
13
src/wuttafarm/web/templates/deform/animaltyperef.pt
Normal file
13
src/wuttafarm/web/templates/deform/animaltyperef.pt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
||||||
13
src/wuttafarm/web/templates/deform/planttyperefs.pt
Normal file
13
src/wuttafarm/web/templates/deform/planttyperefs.pt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
||||||
324
src/wuttafarm/web/templates/wuttafarm-components.mako
Normal file
324
src/wuttafarm/web/templates/wuttafarm-components.mako
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
|
||||||
|
<%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" /> 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>
|
||||||
|
|
@ -23,9 +23,29 @@
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@ 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):
|
||||||
|
|
@ -42,6 +44,8 @@ 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 = [
|
||||||
|
|
@ -58,8 +62,8 @@ class AnimalTypeView(AssetTypeMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
|
|
@ -103,6 +107,19 @@ 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()
|
||||||
|
|
@ -125,6 +142,7 @@ 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")
|
||||||
|
|
@ -134,6 +152,55 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -145,6 +212,7 @@ 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",
|
||||||
|
|
@ -160,6 +228,9 @@ class AnimalAssetView(AssetMasterView):
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"is_sterile",
|
"is_sterile",
|
||||||
"sex",
|
"sex",
|
||||||
|
"groups",
|
||||||
|
"owners",
|
||||||
|
"locations",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -172,9 +243,12 @@ class AnimalAssetView(AssetMasterView):
|
||||||
"is_sterile",
|
"is_sterile",
|
||||||
"notes",
|
"notes",
|
||||||
"asset_type",
|
"asset_type",
|
||||||
|
"owners",
|
||||||
|
"locations",
|
||||||
|
"groups",
|
||||||
"archived",
|
"archived",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
"thumbnail_url",
|
"thumbnail_url",
|
||||||
"image_url",
|
"image_url",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
|
|
@ -201,6 +275,7 @@ 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)
|
||||||
|
|
@ -216,7 +291,7 @@ class AnimalAssetView(AssetMasterView):
|
||||||
f.set_node("animal_type", AnimalTypeRef(self.request))
|
f.set_node("animal_type", AnimalTypeRef(self.request))
|
||||||
|
|
||||||
# sex
|
# sex
|
||||||
if self.viewing and animal.sex is None:
|
if not (self.creating or self.editing) 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))
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class AssetTypeView(WuttaFarmMasterView):
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"name",
|
"name",
|
||||||
|
"drupal_id",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -50,8 +51,8 @@ class AssetTypeView(WuttaFarmMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
@ -78,6 +79,19 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,17 @@ 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
|
from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs
|
||||||
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):
|
||||||
|
|
@ -45,79 +49,6 @@ 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.
|
||||||
|
|
@ -133,6 +64,12 @@ 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 = {
|
||||||
|
|
@ -175,7 +112,10 @@ 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()
|
||||||
return session.query(model_class).join(model.Asset)
|
query = session.query(model_class)
|
||||||
|
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
|
||||||
|
|
@ -200,15 +140,77 @@ 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:
|
||||||
|
|
@ -218,6 +220,7 @@ 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
|
||||||
|
|
@ -230,12 +233,39 @@ 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.parent_uuid for p in asset.asset._parents])
|
f.set_default("parents", [p.uuid for p in asset.parents])
|
||||||
|
|
||||||
# notes
|
# notes
|
||||||
f.set_widget("notes", "notes")
|
f.set_widget("notes", "notes")
|
||||||
|
|
@ -266,11 +296,14 @@ class AssetMasterView(WuttaFarmMasterView):
|
||||||
asset = super().objectify(form)
|
asset = super().objectify(form)
|
||||||
|
|
||||||
if self.creating:
|
if self.creating:
|
||||||
model_class = self.get_model_class()
|
asset.asset_type = self.get_asset_type()
|
||||||
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}")
|
||||||
|
|
||||||
|
|
@ -278,7 +311,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_model_class().__wutta_hint__["farmos_asset_type"]
|
asset_type = self.get_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(
|
||||||
|
|
@ -289,6 +322,21 @@ 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()
|
||||||
|
|
@ -301,7 +349,12 @@ 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)
|
||||||
|
|
@ -318,16 +371,62 @@ 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()
|
||||||
|
|
||||||
AssetView = kwargs.get("AssetView", base["AssetView"])
|
AllAssetView = kwargs.get("AllAssetView", base["AllAssetView"])
|
||||||
AssetView.defaults(config)
|
AllAssetView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,10 @@ 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="farm",
|
client_id=farmos.get_oauth2_client_id(),
|
||||||
scope="farm_manager",
|
scope=farmos.get_oauth2_scope(),
|
||||||
redirect_uri=self.request.route_url("farmos_oauth_callback"),
|
redirect_uri=self.request.route_url("farmos_oauth_callback"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,20 @@ 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",
|
||||||
|
|
@ -121,6 +131,7 @@ 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",
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ from wuttafarm.web.grids import (
|
||||||
NullableBooleanFilter,
|
NullableBooleanFilter,
|
||||||
DateTimeFilter,
|
DateTimeFilter,
|
||||||
)
|
)
|
||||||
from wuttafarm.web.forms.schema import FarmOSRef
|
from wuttafarm.web.forms.schema import FarmOSRef, FarmOSAssetRefs
|
||||||
|
|
||||||
|
|
||||||
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["group_objects"]:
|
for group in animal["groups"]:
|
||||||
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,6 +209,7 @@ 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"])
|
||||||
|
|
@ -218,7 +219,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"],
|
||||||
"group_objects": group_objects,
|
"groups": 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,
|
||||||
|
|
@ -273,6 +274,8 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -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,6 +104,7 @@ 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
|
||||||
|
|
@ -239,6 +240,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
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
|
||||||
|
|
@ -59,6 +60,7 @@ 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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +70,7 @@ class LogMasterView(FarmOSMasterView):
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"name",
|
"name",
|
||||||
"assets",
|
"assets",
|
||||||
|
"locations",
|
||||||
"quantities",
|
"quantities",
|
||||||
"is_group_assignment",
|
"is_group_assignment",
|
||||||
"owners",
|
"owners",
|
||||||
|
|
@ -84,17 +87,21 @@ 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", "owner"}
|
return {"log_type", "quantity", "asset", "group", "location", "owner"}
|
||||||
|
|
||||||
def get_grid_data(self, **kwargs):
|
def get_grid_data(self, **kwargs):
|
||||||
return ResourceData(
|
return ResourceData(
|
||||||
|
|
@ -139,6 +146,12 @@ 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)
|
||||||
|
|
||||||
|
|
@ -152,6 +165,9 @@ 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:
|
||||||
|
|
@ -210,9 +226,21 @@ 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())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ class HarvestLogView(LogMasterView):
|
||||||
"name",
|
"name",
|
||||||
"assets",
|
"assets",
|
||||||
"quantities",
|
"quantities",
|
||||||
"is_group_assignment",
|
|
||||||
"owners",
|
"owners",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ 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):
|
||||||
|
|
@ -41,6 +42,35 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,18 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -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 save_farmos_oauth2_token, use_farmos_style_grid_links
|
from wuttafarm.web.util import get_farmos_client_for_user, use_farmos_style_grid_links
|
||||||
from wuttafarm.web.grids import (
|
from wuttafarm.web.grids import (
|
||||||
ResourceData,
|
ResourceData,
|
||||||
StringFilter,
|
StringFilter,
|
||||||
|
|
@ -70,28 +70,12 @@ 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 = self.get_farmos_client()
|
self.farmos_client = get_farmos_client_for_user(self.request)
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
||||||
|
from wuttafarm.web.grids import ResourceData
|
||||||
|
|
||||||
|
|
||||||
class QuantityTypeView(FarmOSMasterView):
|
class QuantityTypeView(FarmOSMasterView):
|
||||||
|
|
@ -130,13 +131,15 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
farmos_quantity_type = None
|
farmos_quantity_type = None
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
"drupal_id",
|
||||||
|
"as_text",
|
||||||
"measure",
|
"measure",
|
||||||
"value",
|
"value",
|
||||||
|
"unit",
|
||||||
"label",
|
"label",
|
||||||
"changed",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = ("changed", "desc")
|
sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"measure",
|
"measure",
|
||||||
|
|
@ -147,20 +150,58 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
"changed",
|
"changed",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_grid_data(self, columns=None, session=None):
|
def get_farmos_api_includes(self):
|
||||||
result = self.farmos_client.resource.get("quantity", self.farmos_quantity_type)
|
return {"units"}
|
||||||
return [self.normalize_quantity(t) for t in result["data"]]
|
|
||||||
|
def get_grid_data(self, **kwargs):
|
||||||
|
return ResourceData(
|
||||||
|
self.config,
|
||||||
|
self.farmos_client,
|
||||||
|
f"quantity--{self.farmos_quantity_type}",
|
||||||
|
include=",".join(self.get_farmos_api_includes()),
|
||||||
|
normalizer=self.normalize_quantity,
|
||||||
|
)
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# drupal_id
|
||||||
|
g.set_label("drupal_id", "ID", column_only=True)
|
||||||
|
|
||||||
|
# as_text
|
||||||
|
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||||
|
|
||||||
|
# measure
|
||||||
|
g.set_renderer("measure", self.render_measure_for_grid)
|
||||||
|
|
||||||
# value
|
# value
|
||||||
g.set_link("value")
|
g.set_renderer("value", self.render_value_for_grid)
|
||||||
|
|
||||||
|
# unit
|
||||||
|
g.set_renderer("unit", self.render_unit_for_grid)
|
||||||
|
|
||||||
# changed
|
# changed
|
||||||
g.set_renderer("changed", "datetime")
|
g.set_renderer("changed", "datetime")
|
||||||
|
|
||||||
|
def render_as_text_for_grid(self, qty, field, value):
|
||||||
|
measure = qty["measure"].capitalize()
|
||||||
|
value = qty["value"]["decimal"]
|
||||||
|
units = qty["unit"]["name"] if qty["unit"] else "??"
|
||||||
|
return f"( {measure} ) {value} {units}"
|
||||||
|
|
||||||
|
def render_measure_for_grid(self, qty, field, value):
|
||||||
|
return qty["measure"].capitalize()
|
||||||
|
|
||||||
|
def render_unit_for_grid(self, qty, field, value):
|
||||||
|
unit = qty[field]
|
||||||
|
if not unit:
|
||||||
|
return ""
|
||||||
|
return unit["name"]
|
||||||
|
|
||||||
|
def render_value_for_grid(self, qty, field, value):
|
||||||
|
return qty["value"]["decimal"]
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
quantity = self.farmos_client.resource.get_id(
|
quantity = self.farmos_client.resource.get_id(
|
||||||
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
|
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
|
||||||
|
|
@ -187,7 +228,7 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
def get_instance_title(self, quantity):
|
def get_instance_title(self, quantity):
|
||||||
return quantity["value"]
|
return quantity["value"]
|
||||||
|
|
||||||
def normalize_quantity(self, quantity):
|
def normalize_quantity(self, quantity, included={}):
|
||||||
|
|
||||||
if created := quantity["attributes"]["created"]:
|
if created := quantity["attributes"]["created"]:
|
||||||
created = datetime.datetime.fromisoformat(created)
|
created = datetime.datetime.fromisoformat(created)
|
||||||
|
|
@ -197,11 +238,37 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
changed = datetime.datetime.fromisoformat(changed)
|
changed = datetime.datetime.fromisoformat(changed)
|
||||||
changed = self.app.localtime(changed)
|
changed = self.app.localtime(changed)
|
||||||
|
|
||||||
|
quantity_type_object = None
|
||||||
|
quantity_type_uuid = None
|
||||||
|
unit_object = None
|
||||||
|
unit_uuid = None
|
||||||
|
if relationships := quantity["relationships"]:
|
||||||
|
|
||||||
|
if quantity_type := relationships["quantity_type"]["data"]:
|
||||||
|
quantity_type_uuid = quantity_type["id"]
|
||||||
|
quantity_type_object = {
|
||||||
|
"uuid": quantity_type_uuid,
|
||||||
|
"type": "quantity_type--quantity_type",
|
||||||
|
}
|
||||||
|
|
||||||
|
if unit := relationships["units"]["data"]:
|
||||||
|
unit_uuid = unit["id"]
|
||||||
|
if unit := included.get(unit_uuid):
|
||||||
|
unit_object = {
|
||||||
|
"uuid": unit_uuid,
|
||||||
|
"type": "taxonomy_term--unit",
|
||||||
|
"name": unit["attributes"]["name"],
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": quantity["id"],
|
"uuid": quantity["id"],
|
||||||
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
||||||
|
"quantity_type": quantity_type_object,
|
||||||
|
"quantity_type_uuid": quantity_type_uuid,
|
||||||
"measure": quantity["attributes"]["measure"],
|
"measure": quantity["attributes"]["measure"],
|
||||||
"value": quantity["attributes"]["value"],
|
"value": quantity["attributes"]["value"],
|
||||||
|
"unit": unit_object,
|
||||||
|
"unit_uuid": unit_uuid,
|
||||||
"label": quantity["attributes"]["label"] or colander.null,
|
"label": quantity["attributes"]["label"] or colander.null,
|
||||||
"created": created,
|
"created": created,
|
||||||
"changed": changed,
|
"changed": changed,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ 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",
|
||||||
|
|
@ -52,8 +53,8 @@ class GroupView(AssetMasterView):
|
||||||
"asset_type",
|
"asset_type",
|
||||||
"produces_eggs",
|
"produces_eggs",
|
||||||
"archived",
|
"archived",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ class LandTypeView(AssetTypeMasterView):
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
|
|
@ -129,6 +129,19 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -139,6 +152,7 @@ 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 = [
|
||||||
|
|
@ -159,8 +173,8 @@ class LandAssetView(AssetMasterView):
|
||||||
"is_location",
|
"is_location",
|
||||||
"is_fixed",
|
"is_fixed",
|
||||||
"archived",
|
"archived",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ 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
|
||||||
|
|
@ -33,18 +34,8 @@ 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 LogAssetRefs
|
from wuttafarm.web.forms.schema import AssetRefs, LogQuantityRefs, OwnerRefs
|
||||||
|
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):
|
||||||
|
|
@ -70,8 +61,8 @@ class LogTypeView(WuttaFarmMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
@ -99,85 +90,17 @@ 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",
|
||||||
"owners": "Owner",
|
"locations": "Location",
|
||||||
|
"quantities": "Quantity",
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
|
@ -186,8 +109,8 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"message",
|
"message",
|
||||||
"assets",
|
"assets",
|
||||||
# "location",
|
"locations",
|
||||||
"quantity",
|
"quantities",
|
||||||
"is_group_assignment",
|
"is_group_assignment",
|
||||||
"owners",
|
"owners",
|
||||||
]
|
]
|
||||||
|
|
@ -196,21 +119,25 @@ 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",
|
||||||
"location",
|
"groups",
|
||||||
"quantity",
|
"locations",
|
||||||
|
"quantities",
|
||||||
"notes",
|
"notes",
|
||||||
"status",
|
"status",
|
||||||
"log_type",
|
"log_type",
|
||||||
"owners",
|
"owners",
|
||||||
|
"is_movement",
|
||||||
"is_group_assignment",
|
"is_group_assignment",
|
||||||
"farmos_uuid",
|
"quick",
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_query(self, session=None):
|
def get_query(self, session=None):
|
||||||
|
|
@ -218,16 +145,26 @@ 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()
|
||||||
return session.query(model_class).join(model.Log)
|
query = session.query(model_class)
|
||||||
|
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("status", model.Log.status)
|
g.set_filter(
|
||||||
|
"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)
|
||||||
|
|
@ -248,13 +185,68 @@ 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):
|
||||||
return ", ".join([a.asset.asset_name for a in log.log._assets])
|
assets = getattr(log, field)
|
||||||
|
|
||||||
|
if self.farmos_style_grid_links:
|
||||||
|
links = []
|
||||||
|
for asset in assets:
|
||||||
|
url = self.request.route_url(
|
||||||
|
f"{asset.asset_type}_assets.view", uuid=asset.uuid
|
||||||
|
)
|
||||||
|
links.append(tags.link_to(str(asset), url))
|
||||||
|
return ", ".join(links)
|
||||||
|
|
||||||
|
return ", ".join([str(a) for a in assets])
|
||||||
|
|
||||||
|
def render_quantities_for_grid(self, log, field, value):
|
||||||
|
quantities = getattr(log, field) or []
|
||||||
|
items = []
|
||||||
|
for qty in quantities:
|
||||||
|
items.append(HTML.tag("li", c=qty.render_as_text(self.config)))
|
||||||
|
return HTML.tag("ul", c=items)
|
||||||
|
|
||||||
|
def render_owners_for_grid(self, log, field, value):
|
||||||
|
|
||||||
|
if self.farmos_style_grid_links:
|
||||||
|
links = []
|
||||||
|
for user in log.owners:
|
||||||
|
url = self.request.route_url("users.view", uuid=user.uuid)
|
||||||
|
links.append(tags.link_to(user.username, url))
|
||||||
|
return ", ".join(links)
|
||||||
|
|
||||||
|
return ", ".join([user.username for user in log.owners])
|
||||||
|
|
||||||
|
def grid_row_class(self, log, data, i):
|
||||||
|
if log.status == "pending":
|
||||||
|
return "has-background-warning"
|
||||||
|
if log.status == "abandoned":
|
||||||
|
return "has-background-danger"
|
||||||
|
return None
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -267,12 +259,25 @@ 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", LogAssetRefs(self.request))
|
f.set_node("assets", AssetRefs(self.request))
|
||||||
f.set_default("assets", [a.asset_uuid for a in log.log._assets])
|
# nb. must explicity declare value for non-standard field
|
||||||
|
f.set_default("assets", log.assets)
|
||||||
|
|
||||||
# location
|
# groups
|
||||||
if self.creating or self.editing:
|
if self.creating or self.editing:
|
||||||
f.remove("location") # TODO: need to support this
|
f.remove("groups") # TODO: need to support this
|
||||||
|
else:
|
||||||
|
f.set_node("groups", AssetRefs(self.request))
|
||||||
|
# nb. must explicity declare value for non-standard field
|
||||||
|
f.set_default("groups", log.groups)
|
||||||
|
|
||||||
|
# locations
|
||||||
|
if self.creating or self.editing:
|
||||||
|
f.remove("locations") # TODO: need to support this
|
||||||
|
else:
|
||||||
|
f.set_node("locations", AssetRefs(self.request))
|
||||||
|
# nb. must explicity declare value for non-standard field
|
||||||
|
f.set_default("locations", log.locations)
|
||||||
|
|
||||||
# log_type
|
# log_type
|
||||||
if self.creating:
|
if self.creating:
|
||||||
|
|
@ -280,13 +285,19 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
else:
|
else:
|
||||||
f.set_node(
|
f.set_node(
|
||||||
"log_type",
|
"log_type",
|
||||||
WuttaDictEnum(self.request, get_log_type_enum(self.config)),
|
WuttaDictEnum(
|
||||||
|
self.request, get_log_type_enum(self.config, session=session)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
f.set_readonly("log_type")
|
f.set_readonly("log_type")
|
||||||
|
|
||||||
# quantity
|
# quantities
|
||||||
if self.creating or self.editing:
|
if self.creating or self.editing:
|
||||||
f.remove("quantity") # TODO: need to support this
|
f.remove("quantities") # TODO: need to support this
|
||||||
|
else:
|
||||||
|
f.set_node("quantities", LogQuantityRefs(self.request))
|
||||||
|
# nb. must explicity declare value for non-standard field
|
||||||
|
f.set_default("quantities", log.quantities)
|
||||||
|
|
||||||
# notes
|
# notes
|
||||||
f.set_widget("notes", "notes")
|
f.set_widget("notes", "notes")
|
||||||
|
|
@ -294,13 +305,23 @@ 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)
|
||||||
|
|
||||||
|
|
@ -333,6 +354,68 @@ 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()
|
||||||
|
|
@ -340,8 +423,8 @@ def defaults(config, **kwargs):
|
||||||
LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"])
|
LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"])
|
||||||
LogTypeView.defaults(config)
|
LogTypeView.defaults(config)
|
||||||
|
|
||||||
LogView = kwargs.get("LogView", base["LogView"])
|
AllLogView = kwargs.get("AllLogView", base["AllLogView"])
|
||||||
LogView.defaults(config)
|
AllLogView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ 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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,19 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,29 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,21 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
from wuttafarm.web.util import use_farmos_style_grid_links, get_farmos_client_for_user
|
||||||
|
|
||||||
|
|
||||||
class WuttaFarmMasterView(MasterView):
|
class WuttaFarmMasterView(MasterView):
|
||||||
|
|
@ -36,6 +36,8 @@ 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",
|
||||||
|
|
@ -104,5 +106,42 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,16 @@
|
||||||
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):
|
||||||
|
|
@ -40,6 +44,8 @@ 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 = [
|
||||||
|
|
@ -56,8 +62,8 @@ class PlantTypeView(AssetTypeMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
|
|
@ -98,6 +104,19 @@ 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()
|
||||||
|
|
@ -126,6 +145,55 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -136,6 +204,7 @@ 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 = {
|
||||||
|
|
@ -158,8 +227,8 @@ class PlantAssetView(AssetMasterView):
|
||||||
"notes",
|
"notes",
|
||||||
"asset_type",
|
"asset_type",
|
||||||
"archived",
|
"archived",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
"thumbnail_url",
|
"thumbnail_url",
|
||||||
"image_url",
|
"image_url",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
|
|
@ -171,10 +240,20 @@ class PlantAssetView(AssetMasterView):
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# plant_types
|
# plant_types
|
||||||
g.set_renderer("plant_types", self.render_grid_plant_types)
|
g.set_renderer("plant_types", self.render_plant_types_for_grid)
|
||||||
|
|
||||||
def render_grid_plant_types(self, plant, field, value):
|
def render_plant_types_for_grid(self, plant, field, value):
|
||||||
return ", ".join([t.plant_type.name for t in plant._plant_types])
|
plant_types = plant._plant_types
|
||||||
|
|
||||||
|
if self.farmos_style_grid_links:
|
||||||
|
links = []
|
||||||
|
for plant_type in plant_types:
|
||||||
|
plant_type = plant_type.plant_type
|
||||||
|
url = self.request.route_url("plant_types.view", uuid=plant_type.uuid)
|
||||||
|
links.append(tags.link_to(str(plant_type), url))
|
||||||
|
return ", ".join(links)
|
||||||
|
|
||||||
|
return ", ".join([str(pt.plant_type) for pt in plant_types])
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
f = form
|
f = form
|
||||||
|
|
@ -183,18 +262,38 @@ 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))
|
||||||
f.set_default(
|
if not self.creating:
|
||||||
"plant_types", [t.plant_type_uuid for t in plant._plant_types]
|
# nb. must explcitly declare value for non-standard field
|
||||||
)
|
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()
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
|
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
|
||||||
from wuttafarm.web.forms.schema import UnitRef
|
from wuttafarm.web.forms.schema import UnitRef, LogRef
|
||||||
|
|
||||||
|
|
||||||
def get_quantity_type_enum(config):
|
def get_quantity_type_enum(config):
|
||||||
|
|
@ -66,8 +66,8 @@ class QuantityTypeView(WuttaFarmMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
@ -119,8 +119,9 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
"value",
|
"value",
|
||||||
"units",
|
"units",
|
||||||
"label",
|
"label",
|
||||||
"farmos_uuid",
|
"log",
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_query(self, session=None):
|
def get_query(self, session=None):
|
||||||
|
|
@ -231,6 +232,13 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
# TODO: ugh
|
# TODO: ugh
|
||||||
f.set_default("units", quantity.quantity.units)
|
f.set_default("units", quantity.quantity.units)
|
||||||
|
|
||||||
|
# log
|
||||||
|
if self.creating or self.editing:
|
||||||
|
f.remove("log")
|
||||||
|
else:
|
||||||
|
f.set_node("log", LogRef(self.request))
|
||||||
|
f.set_default("log", quantity.log)
|
||||||
|
|
||||||
def get_xref_buttons(self, quantity):
|
def get_xref_buttons(self, quantity):
|
||||||
buttons = super().get_xref_buttons(quantity)
|
buttons = super().get_xref_buttons(quantity)
|
||||||
|
|
||||||
|
|
@ -248,7 +256,7 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
class QuantityView(QuantityMasterView):
|
class AllQuantityView(QuantityMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for All Quantities
|
Master view for All Quantities
|
||||||
"""
|
"""
|
||||||
|
|
@ -280,8 +288,8 @@ def defaults(config, **kwargs):
|
||||||
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
|
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
|
||||||
QuantityTypeView.defaults(config)
|
QuantityTypeView.defaults(config)
|
||||||
|
|
||||||
QuantityView = kwargs.get("QuantityView", base["QuantityView"])
|
AllQuantityView = kwargs.get("AllQuantityView", base["AllQuantityView"])
|
||||||
QuantityView.defaults(config)
|
AllQuantityView.defaults(config)
|
||||||
|
|
||||||
StandardQuantityView = kwargs.get(
|
StandardQuantityView = kwargs.get(
|
||||||
"StandardQuantityView", base["StandardQuantityView"]
|
"StandardQuantityView", base["StandardQuantityView"]
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,9 @@ 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")
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,9 @@ 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 save_farmos_oauth2_token
|
from wuttafarm.web.util import get_farmos_client_for_user
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -40,9 +41,11 @@ 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 = self.get_farmos_client()
|
self.farmos_client = get_farmos_client_for_user(self.request)
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -127,22 +130,6 @@ 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)
|
||||||
|
|
@ -151,6 +138,10 @@ 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(cls, route_name=f"quick.{route_slug}")
|
config.add_view(
|
||||||
|
cls, route_name=f"quick.{route_slug}", permission=f"quick.{route_slug}"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ class EggsQuickForm(QuickFormView):
|
||||||
|
|
||||||
_layer_assets = None
|
_layer_assets = None
|
||||||
|
|
||||||
|
# TODO: make this configurable?
|
||||||
|
unit_name = "egg(s)"
|
||||||
|
|
||||||
def make_quick_form(self):
|
def make_quick_form(self):
|
||||||
f = self.make_form(
|
f = self.make_form(
|
||||||
fields=[
|
fields=[
|
||||||
|
|
@ -88,6 +91,47 @@ class EggsQuickForm(QuickFormView):
|
||||||
if self._layer_assets is not None:
|
if self._layer_assets is not None:
|
||||||
return self._layer_assets
|
return self._layer_assets
|
||||||
|
|
||||||
|
if self.app.is_farmos_wrapper():
|
||||||
|
assets = self.get_layer_assets_from_farmos()
|
||||||
|
else:
|
||||||
|
assets = self.get_layer_assets_from_wuttafarm()
|
||||||
|
|
||||||
|
assets.sort(key=lambda a: a["name"])
|
||||||
|
self._layer_assets = assets
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def get_layer_assets_from_wuttafarm(self):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
assets = []
|
||||||
|
|
||||||
|
def normalize(asset):
|
||||||
|
asset_type = asset.__wutta_hint__["farmos_asset_type"]
|
||||||
|
return {
|
||||||
|
"uuid": str(asset.farmos_uuid),
|
||||||
|
"name": asset.asset_name,
|
||||||
|
"type": f"asset--{asset_type}",
|
||||||
|
}
|
||||||
|
|
||||||
|
query = (
|
||||||
|
session.query(model.AnimalAsset)
|
||||||
|
.join(model.Asset)
|
||||||
|
.filter(model.AnimalAsset.produces_eggs == True)
|
||||||
|
.order_by(model.Asset.asset_name)
|
||||||
|
)
|
||||||
|
assets.extend([normalize(a) for a in query])
|
||||||
|
|
||||||
|
query = (
|
||||||
|
session.query(model.GroupAsset)
|
||||||
|
.join(model.Asset)
|
||||||
|
.filter(model.GroupAsset.produces_eggs == True)
|
||||||
|
.order_by(model.Asset.asset_name)
|
||||||
|
)
|
||||||
|
assets.extend([normalize(a) for a in query])
|
||||||
|
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def get_layer_assets_from_farmos(self):
|
||||||
assets = []
|
assets = []
|
||||||
params = {
|
params = {
|
||||||
"filter[produces_eggs]": 1,
|
"filter[produces_eggs]": 1,
|
||||||
|
|
@ -107,21 +151,14 @@ class EggsQuickForm(QuickFormView):
|
||||||
result = self.farmos_client.asset.get("group", params=params)
|
result = self.farmos_client.asset.get("group", params=params)
|
||||||
assets.extend([normalize(a) for a in result["data"]])
|
assets.extend([normalize(a) for a in result["data"]])
|
||||||
|
|
||||||
assets.sort(key=lambda a: a["name"])
|
|
||||||
self._layer_assets = assets
|
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
def save_quick_form(self, form):
|
def save_quick_form(self, form):
|
||||||
|
|
||||||
response = self.save_to_farmos(form)
|
if self.app.is_farmos_wrapper():
|
||||||
log = json.loads(response["create-log#body{0}"]["body"])
|
return self.save_to_farmos(form)
|
||||||
|
|
||||||
if self.app.is_farmos_mirror():
|
return self.save_to_wuttafarm(form)
|
||||||
quantity = json.loads(response["create-quantity"]["body"])
|
|
||||||
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
|
||||||
|
|
@ -131,7 +168,7 @@ class EggsQuickForm(QuickFormView):
|
||||||
asset = assets[data["asset"]]
|
asset = assets[data["asset"]]
|
||||||
|
|
||||||
# TODO: make this configurable?
|
# TODO: make this configurable?
|
||||||
unit_name = "egg(s)"
|
unit_name = self.unit_name
|
||||||
|
|
||||||
unit = {"data": {"type": "taxonomy_term--unit"}}
|
unit = {"data": {"type": "taxonomy_term--unit"}}
|
||||||
new_unit = None
|
new_unit = None
|
||||||
|
|
@ -192,6 +229,7 @@ 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"],
|
||||||
},
|
},
|
||||||
|
|
@ -229,13 +267,87 @@ class EggsQuickForm(QuickFormView):
|
||||||
blueprints.insert(0, new_unit)
|
blueprints.insert(0, new_unit)
|
||||||
blueprint = SubrequestsBlueprint.parse_obj(blueprints)
|
blueprint = SubrequestsBlueprint.parse_obj(blueprints)
|
||||||
response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
|
response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
|
||||||
return response
|
|
||||||
|
|
||||||
def redirect_after_save(self, result):
|
log = json.loads(response["create-log#body{0}"]["body"])
|
||||||
return self.redirect(
|
|
||||||
self.request.route_url(
|
if self.app.is_farmos_mirror():
|
||||||
"farmos_logs_harvest.view", uuid=result["data"]["id"]
|
if new_unit:
|
||||||
|
unit = json.loads(response["create-unit"]["body"])
|
||||||
|
self.app.auto_sync_from_farmos(
|
||||||
|
unit["data"], "Unit", client=self.farmos_client
|
||||||
)
|
)
|
||||||
|
quantity = json.loads(response["create-quantity"]["body"])
|
||||||
|
self.app.auto_sync_from_farmos(
|
||||||
|
quantity["data"], "StandardQuantity", client=self.farmos_client
|
||||||
|
)
|
||||||
|
self.app.auto_sync_from_farmos(
|
||||||
|
log["data"], "HarvestLog", client=self.farmos_client
|
||||||
|
)
|
||||||
|
|
||||||
|
return log
|
||||||
|
|
||||||
|
def save_to_wuttafarm(self, form):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
data = form.validated
|
||||||
|
|
||||||
|
asset = (
|
||||||
|
session.query(model.Asset)
|
||||||
|
.filter(model.Asset.farmos_uuid == data["asset"])
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: make this configurable?
|
||||||
|
unit_name = self.unit_name
|
||||||
|
|
||||||
|
new_unit = False
|
||||||
|
unit = session.query(model.Unit).filter(model.Unit.name == unit_name).first()
|
||||||
|
if not unit:
|
||||||
|
unit = model.Unit(name=unit_name)
|
||||||
|
session.add(unit)
|
||||||
|
new_unit = True
|
||||||
|
|
||||||
|
quantity = model.StandardQuantity(
|
||||||
|
quantity_type_id="standard",
|
||||||
|
measure_id="count",
|
||||||
|
value_numerator=data["count"],
|
||||||
|
value_denominator=1,
|
||||||
|
units=unit,
|
||||||
|
)
|
||||||
|
session.add(quantity)
|
||||||
|
|
||||||
|
log = model.HarvestLog(
|
||||||
|
log_type="harvest",
|
||||||
|
message=f"Collected {data['count']} {unit_name}",
|
||||||
|
timestamp=self.app.make_utc(data["timestamp"]),
|
||||||
|
notes=data["notes"] or None,
|
||||||
|
quick="eggs",
|
||||||
|
status="done",
|
||||||
|
)
|
||||||
|
session.add(log)
|
||||||
|
log.assets.append(asset)
|
||||||
|
log.quantities.append(quantity.quantity)
|
||||||
|
log.owners.append(self.request.user)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
if self.app.is_farmos_mirror():
|
||||||
|
if new_unit:
|
||||||
|
self.app.auto_sync_to_farmos(unit, client=self.farmos_client)
|
||||||
|
self.app.auto_sync_to_farmos(quantity, client=self.farmos_client)
|
||||||
|
self.app.auto_sync_to_farmos(log, client=self.farmos_client)
|
||||||
|
|
||||||
|
return log
|
||||||
|
|
||||||
|
def redirect_after_save(self, log):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
if isinstance(log, model.HarvestLog):
|
||||||
|
return self.redirect(
|
||||||
|
self.request.route_url("logs_harvest.view", uuid=log.uuid)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.redirect(
|
||||||
|
self.request.route_url("farmos_logs_harvest.view", uuid=log["data"]["id"])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,21 @@ 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(),
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ class StructureTypeView(AssetTypeMasterView):
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
|
|
@ -128,6 +128,19 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -138,6 +151,7 @@ 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 = [
|
||||||
|
|
@ -146,6 +160,7 @@ class StructureAssetView(AssetMasterView):
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"structure_type",
|
"structure_type",
|
||||||
"parents",
|
"parents",
|
||||||
|
"owners",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -158,8 +173,8 @@ class StructureAssetView(AssetMasterView):
|
||||||
"is_location",
|
"is_location",
|
||||||
"is_fixed",
|
"is_fixed",
|
||||||
"archived",
|
"archived",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
"thumbnail_url",
|
"thumbnail_url",
|
||||||
"image_url",
|
"image_url",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Master view for Units
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import Measure, Unit
|
from wuttafarm.db.model import Measure, Unit, Quantity
|
||||||
|
|
||||||
|
|
||||||
class MeasureView(WuttaFarmMasterView):
|
class MeasureView(WuttaFarmMasterView):
|
||||||
|
|
@ -52,6 +52,26 @@ class MeasureView(WuttaFarmMasterView):
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
row_model_class = Quantity
|
||||||
|
rows_viewable = True
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
"quantity_type_id": "Quantity Type ID",
|
||||||
|
"measure_id": "Measure ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
"drupal_id",
|
||||||
|
"as_text",
|
||||||
|
"quantity_type",
|
||||||
|
"value",
|
||||||
|
"units",
|
||||||
|
"label",
|
||||||
|
]
|
||||||
|
|
||||||
|
rows_sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
@ -59,6 +79,50 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,6 +133,8 @@ 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 = [
|
||||||
|
|
@ -85,10 +151,30 @@ class UnitView(WuttaFarmMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"farmos_uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
row_model_class = Quantity
|
||||||
|
rows_viewable = True
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
"quantity_type_id": "Quantity Type ID",
|
||||||
|
"measure_id": "Measure ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
"drupal_id",
|
||||||
|
"as_text",
|
||||||
|
"quantity_type",
|
||||||
|
"measure",
|
||||||
|
"value",
|
||||||
|
"label",
|
||||||
|
]
|
||||||
|
|
||||||
|
rows_sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
@ -116,6 +202,37 @@ class UnitView(WuttaFarmMasterView):
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
def get_row_grid_data(self, unit):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
return session.query(model.Quantity).filter(model.Quantity.units == unit)
|
||||||
|
|
||||||
|
def configure_row_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_row_grid(g)
|
||||||
|
|
||||||
|
# drupal_id
|
||||||
|
g.set_label("drupal_id", "ID", column_only=True)
|
||||||
|
|
||||||
|
# as_text
|
||||||
|
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||||
|
g.set_link("as_text")
|
||||||
|
|
||||||
|
# value
|
||||||
|
g.set_renderer("value", self.render_value_for_grid)
|
||||||
|
|
||||||
|
def render_as_text_for_grid(self, quantity, field, value):
|
||||||
|
return quantity.render_as_text(self.config)
|
||||||
|
|
||||||
|
def render_value_for_grid(self, quantity, field, value):
|
||||||
|
value = quantity.value_numerator / quantity.value_denominator
|
||||||
|
return self.app.render_quantity(value)
|
||||||
|
|
||||||
|
def get_row_action_url_view(self, quantity, i):
|
||||||
|
return self.request.route_url(
|
||||||
|
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,13 @@ 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):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue