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