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/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v0.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)
|
## v0.8.0 (2026-03-04)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaFarm"
|
name = "WuttaFarm"
|
||||||
version = "0.8.0"
|
version = "0.11.2"
|
||||||
description = "Web app to integrate with and extend farmOS"
|
description = "Web app to integrate with and extend farmOS"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|
@ -34,7 +34,7 @@ dependencies = [
|
||||||
"pyramid_exclog",
|
"pyramid_exclog",
|
||||||
"uvicorn[standard]",
|
"uvicorn[standard]",
|
||||||
"WuttaSync",
|
"WuttaSync",
|
||||||
"WuttaWeb[continuum]>=0.29.0",
|
"WuttaWeb[continuum]>=0.30.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,74 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
factory = self.load_object(spec)
|
factory = self.load_object(spec)
|
||||||
return factory(self.config, farmos_client)
|
return factory(self.config, farmos_client)
|
||||||
|
|
||||||
|
def 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):
|
def auto_sync_to_farmos(self, obj, model_name=None, client=None, require=True):
|
||||||
"""
|
"""
|
||||||
Export the given object to farmOS, using configured handler.
|
Export the given object to farmOS, using configured handler.
|
||||||
|
|
@ -202,6 +270,7 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
then nothing will happen / import is silently skipped when
|
then nothing will happen / import is silently skipped when
|
||||||
there is no such importer.
|
there is no such importer.
|
||||||
"""
|
"""
|
||||||
|
model = self.app.model
|
||||||
handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos")
|
handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos")
|
||||||
|
|
||||||
if model_name not in handler.importers:
|
if model_name not in handler.importers:
|
||||||
|
|
@ -212,6 +281,10 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
# nb. begin txn to establish the API client
|
# nb. begin txn to establish the API client
|
||||||
handler.begin_source_transaction(client)
|
handler.begin_source_transaction(client)
|
||||||
with self.short_session(commit=True) as session:
|
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
|
handler.target_session = session
|
||||||
importer = handler.get_importer(model_name, caches_target=False)
|
importer = handler.get_importer(model_name, caches_target=False)
|
||||||
normal = importer.normalize_source_object(obj)
|
normal = importer.normalize_source_object(obj)
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,4 @@ from .base import wuttafarm_typer
|
||||||
from . import export_farmos
|
from . import export_farmos
|
||||||
from . import import_farmos
|
from . import import_farmos
|
||||||
from . import install
|
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
|
# wuttafarm proper models
|
||||||
from .unit import Unit, Measure
|
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 import AssetType, Asset, AssetParent
|
||||||
from .asset_land import LandType, LandAsset
|
from .asset_land import LandType, LandAsset
|
||||||
from .asset_structure import StructureType, StructureAsset
|
from .asset_structure import StructureType, StructureAsset
|
||||||
|
from .asset_equipment import EquipmentType, EquipmentAsset, EquipmentAssetEquipmentType
|
||||||
from .asset_animal import AnimalType, AnimalAsset
|
from .asset_animal import AnimalType, AnimalAsset
|
||||||
from .asset_group import GroupAsset
|
from .asset_group import GroupAsset
|
||||||
from .asset_plant import PlantType, PlantAsset, PlantAssetPlantType
|
from .asset_plant import (
|
||||||
|
PlantType,
|
||||||
|
Season,
|
||||||
|
PlantAsset,
|
||||||
|
PlantAssetPlantType,
|
||||||
|
PlantAssetSeason,
|
||||||
|
)
|
||||||
|
from .asset_water import WaterAsset
|
||||||
from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner
|
from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner
|
||||||
from .log_activity import ActivityLog
|
from .log_activity import ActivityLog
|
||||||
from .log_harvest import HarvestLog
|
from .log_harvest import HarvestLog
|
||||||
from .log_medical import MedicalLog
|
from .log_medical import MedicalLog
|
||||||
from .log_observation import ObservationLog
|
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 ""
|
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):
|
class PlantAsset(AssetMixin, model.Base):
|
||||||
"""
|
"""
|
||||||
Represents a plant asset from farmOS
|
Represents a plant asset from farmOS
|
||||||
|
|
@ -117,6 +176,19 @@ class PlantAsset(AssetMixin, model.Base):
|
||||||
creator=lambda pt: PlantAssetPlantType(plant_type=pt),
|
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)
|
add_asset_proxies(PlantAsset)
|
||||||
|
|
||||||
|
|
@ -146,3 +218,30 @@ class PlantAssetPlantType(model.Base):
|
||||||
""",
|
""",
|
||||||
back_populates="_plant_assets",
|
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,
|
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):
|
def render_as_text(self, config=None):
|
||||||
measure = str(self.measure or self.measure_id or "")
|
measure = str(self.measure or self.measure_id or "")
|
||||||
value = self.value_numerator / self.value_denominator
|
value = self.get_value_decimal()
|
||||||
if config:
|
if config:
|
||||||
app = config.get_app()
|
app = config.get_app()
|
||||||
value = app.render_quantity(value)
|
value = app.render_quantity(value)
|
||||||
|
|
@ -200,7 +204,15 @@ class QuantityMixin:
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def quantity(cls):
|
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):
|
def render_as_text(self, config=None):
|
||||||
return self.quantity.render_as_text(config)
|
return self.quantity.render_as_text(config)
|
||||||
|
|
@ -240,3 +252,64 @@ class StandardQuantity(QuantityMixin, model.Base):
|
||||||
|
|
||||||
|
|
||||||
add_quantity_proxies(StandardQuantity)
|
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()
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
ordinal = sa.Column(
|
||||||
|
sa.Integer(),
|
||||||
|
nullable=True,
|
||||||
|
doc="""
|
||||||
|
Ordinal (sequence number) for the measure.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
name = sa.Column(
|
name = sa.Column(
|
||||||
sa.String(length=100),
|
sa.String(length=100),
|
||||||
nullable=False,
|
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 = [
|
supported_fields = [
|
||||||
"uuid",
|
"uuid",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_target_objects(self, **kwargs):
|
def get_target_objects(self, **kwargs):
|
||||||
result = self.farmos_client.resource.get(
|
return list(
|
||||||
"taxonomy_term", self.farmos_taxonomy_type
|
self.farmos_client.resource.iterate(
|
||||||
|
"taxonomy_term", self.farmos_taxonomy_type
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def get_target_object(self, key):
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
|
@ -101,17 +103,24 @@ class ToFarmOSTaxonomy(ToFarmOS):
|
||||||
return result["data"]
|
return result["data"]
|
||||||
|
|
||||||
def normalize_target_object(self, obj):
|
def normalize_target_object(self, obj):
|
||||||
|
if description := obj["attributes"]["description"]:
|
||||||
|
description = description["value"]
|
||||||
return {
|
return {
|
||||||
"uuid": UUID(obj["id"]),
|
"uuid": UUID(obj["id"]),
|
||||||
"name": obj["attributes"]["name"],
|
"name": obj["attributes"]["name"],
|
||||||
|
"description": description,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_term_payload(self, source_data):
|
def get_term_payload(self, source_data):
|
||||||
return {
|
|
||||||
"attributes": {
|
attrs = {}
|
||||||
"name": source_data["name"],
|
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):
|
def create_target_object(self, key, source_data):
|
||||||
if source_data.get("__ignoreme__"):
|
if source_data.get("__ignoreme__"):
|
||||||
|
|
@ -127,9 +136,9 @@ class ToFarmOSTaxonomy(ToFarmOS):
|
||||||
normal["_new_object"] = result["data"]
|
normal["_new_object"] = result["data"]
|
||||||
return normal
|
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:
|
if self.dry_run:
|
||||||
return asset
|
return term
|
||||||
|
|
||||||
payload = self.get_term_payload(source_data)
|
payload = self.get_term_payload(source_data)
|
||||||
payload["id"] = str(source_data["uuid"])
|
payload["id"] = str(source_data["uuid"])
|
||||||
|
|
@ -146,9 +155,12 @@ class ToFarmOSAsset(ToFarmOS):
|
||||||
|
|
||||||
farmos_asset_type = None
|
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):
|
def get_target_objects(self, **kwargs):
|
||||||
assets = self.farmos_client.asset.get(self.farmos_asset_type)
|
return list(self.farmos_client.asset.iterate(self.farmos_asset_type))
|
||||||
return assets["data"]
|
|
||||||
|
|
||||||
def get_target_object(self, key):
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
|
@ -191,18 +203,17 @@ class ToFarmOSAsset(ToFarmOS):
|
||||||
return self.normalize_target_object(result["data"])
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
def normalize_target_object(self, asset):
|
def normalize_target_object(self, asset):
|
||||||
|
normal = self.normal.normalize_farmos_asset(asset)
|
||||||
if notes := asset["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": UUID(asset["id"]),
|
"uuid": UUID(normal["uuid"]),
|
||||||
"asset_name": asset["attributes"]["name"],
|
"asset_name": normal["asset_name"],
|
||||||
"is_location": asset["attributes"]["is_location"],
|
"is_location": normal["is_location"],
|
||||||
"is_fixed": asset["attributes"]["is_fixed"],
|
"is_fixed": normal["is_fixed"],
|
||||||
"produces_eggs": asset["attributes"].get("produces_eggs"),
|
# nb. this is only used for certain asset types
|
||||||
"notes": notes,
|
"produces_eggs": normal["produces_eggs"],
|
||||||
"archived": asset["attributes"]["archived"],
|
"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):
|
def get_asset_payload(self, source_data):
|
||||||
|
|
@ -221,8 +232,18 @@ class ToFarmOSAsset(ToFarmOS):
|
||||||
if "archived" in self.fields:
|
if "archived" in self.fields:
|
||||||
attrs["archived"] = source_data["archived"]
|
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
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -245,6 +266,8 @@ class AnimalAssetImporter(ToFarmOSAsset):
|
||||||
"is_sterile",
|
"is_sterile",
|
||||||
"produces_eggs",
|
"produces_eggs",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
|
"is_location",
|
||||||
|
"is_fixed",
|
||||||
"notes",
|
"notes",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
@ -296,6 +319,80 @@ class AnimalTypeImporter(ToFarmOSTaxonomy):
|
||||||
farmos_taxonomy_type = "animal_type"
|
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):
|
class GroupAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
model_title = "GroupAsset"
|
model_title = "GroupAsset"
|
||||||
|
|
@ -353,6 +450,12 @@ class PlantTypeImporter(ToFarmOSTaxonomy):
|
||||||
farmos_taxonomy_type = "plant_type"
|
farmos_taxonomy_type = "plant_type"
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonImporter(ToFarmOSTaxonomy):
|
||||||
|
|
||||||
|
model_title = "Season"
|
||||||
|
farmos_taxonomy_type = "season"
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetImporter(ToFarmOSAsset):
|
class PlantAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
model_title = "PlantAsset"
|
model_title = "PlantAsset"
|
||||||
|
|
@ -362,6 +465,7 @@ class PlantAssetImporter(ToFarmOSAsset):
|
||||||
"uuid",
|
"uuid",
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"plant_type_uuids",
|
"plant_type_uuids",
|
||||||
|
"season_uuids",
|
||||||
"notes",
|
"notes",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
@ -373,6 +477,9 @@ class PlantAssetImporter(ToFarmOSAsset):
|
||||||
"plant_type_uuids": [
|
"plant_type_uuids": [
|
||||||
UUID(p["id"]) for p in plant["relationships"]["plant_type"]["data"]
|
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
|
return data
|
||||||
|
|
@ -398,6 +505,15 @@ class PlantAssetImporter(ToFarmOSAsset):
|
||||||
"type": "taxonomy_term--plant_type",
|
"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)
|
payload["attributes"].update(attrs)
|
||||||
if rels:
|
if rels:
|
||||||
|
|
@ -443,6 +559,21 @@ class StructureAssetImporter(ToFarmOSAsset):
|
||||||
return payload
|
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
|
# quantity importers
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -569,6 +700,49 @@ class ToFarmOSQuantity(ToFarmOS):
|
||||||
return payload
|
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):
|
class StandardQuantityImporter(ToFarmOSQuantity):
|
||||||
|
|
||||||
model_title = "StandardQuantity"
|
model_title = "StandardQuantity"
|
||||||
|
|
@ -597,6 +771,8 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
"notes",
|
"notes",
|
||||||
"quick",
|
"quick",
|
||||||
"assets",
|
"assets",
|
||||||
|
"locations",
|
||||||
|
"groups",
|
||||||
"quantities",
|
"quantities",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -605,8 +781,7 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
self.normal = self.app.get_normalizer(self.farmos_client)
|
self.normal = self.app.get_normalizer(self.farmos_client)
|
||||||
|
|
||||||
def get_target_objects(self, **kwargs):
|
def get_target_objects(self, **kwargs):
|
||||||
result = self.farmos_client.log.get(self.farmos_log_type)
|
return list(self.farmos_client.log.iterate(self.farmos_log_type))
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def get_target_object(self, key):
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
|
@ -660,6 +835,10 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
"notes": normal["notes"],
|
"notes": normal["notes"],
|
||||||
"quick": normal["quick"],
|
"quick": normal["quick"],
|
||||||
"assets": [(a["asset_type"], UUID(a["uuid"])) for a in normal["assets"]],
|
"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"]],
|
"quantities": [UUID(uuid) for uuid in normal["quantity_uuids"]],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,6 +871,26 @@ class ToFarmOSLog(ToFarmOS):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rels["asset"] = {"data": assets}
|
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:
|
if "quantities" in self.fields:
|
||||||
quantities = []
|
quantities = []
|
||||||
for uuid in source_data["quantities"]:
|
for uuid in source_data["quantities"]:
|
||||||
|
|
@ -756,3 +955,48 @@ class ObservationLogImporter(ToFarmOSLog):
|
||||||
|
|
||||||
model_title = "ObservationLog"
|
model_title = "ObservationLog"
|
||||||
farmos_log_type = "observation"
|
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 = super().define_importers()
|
||||||
importers["LandAsset"] = LandAssetImporter
|
importers["LandAsset"] = LandAssetImporter
|
||||||
importers["StructureAsset"] = StructureAssetImporter
|
importers["StructureAsset"] = StructureAssetImporter
|
||||||
|
importers["WaterAsset"] = WaterAssetImporter
|
||||||
|
importers["EquipmentType"] = EquipmentTypeImporter
|
||||||
|
importers["EquipmentAsset"] = EquipmentAssetImporter
|
||||||
importers["AnimalType"] = AnimalTypeImporter
|
importers["AnimalType"] = AnimalTypeImporter
|
||||||
importers["AnimalAsset"] = AnimalAssetImporter
|
importers["AnimalAsset"] = AnimalAssetImporter
|
||||||
importers["GroupAsset"] = GroupAssetImporter
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
importers["PlantType"] = PlantTypeImporter
|
importers["PlantType"] = PlantTypeImporter
|
||||||
|
importers["Season"] = SeasonImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
importers["PlantAsset"] = PlantAssetImporter
|
||||||
importers["Unit"] = UnitImporter
|
importers["Unit"] = UnitImporter
|
||||||
|
importers["MaterialType"] = MaterialTypeImporter
|
||||||
|
importers["MaterialQuantity"] = MaterialQuantityImporter
|
||||||
importers["StandardQuantity"] = StandardQuantityImporter
|
importers["StandardQuantity"] = StandardQuantityImporter
|
||||||
importers["ActivityLog"] = ActivityLogImporter
|
importers["ActivityLog"] = ActivityLogImporter
|
||||||
importers["HarvestLog"] = HarvestLogImporter
|
importers["HarvestLog"] = HarvestLogImporter
|
||||||
importers["MedicalLog"] = MedicalLogImporter
|
importers["MedicalLog"] = MedicalLogImporter
|
||||||
importers["ObservationLog"] = ObservationLogImporter
|
importers["ObservationLog"] = ObservationLogImporter
|
||||||
|
importers["SeedingLog"] = SeedingLogImporter
|
||||||
return importers
|
return importers
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -134,60 +141,156 @@ class FromWuttaFarm(FromWutta):
|
||||||
return obj
|
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
|
WuttaFarm → farmOS API exporter for Animal Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source_model_class = model.AnimalAsset
|
source_model_class = model.AnimalAsset
|
||||||
|
|
||||||
supported_fields = [
|
def get_supported_fields(self):
|
||||||
"uuid",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"animal_type_uuid",
|
[
|
||||||
"sex",
|
"animal_type_uuid",
|
||||||
"is_sterile",
|
"sex",
|
||||||
"produces_eggs",
|
"is_sterile",
|
||||||
"birthdate",
|
"produces_eggs",
|
||||||
"notes",
|
"birthdate",
|
||||||
"archived",
|
]
|
||||||
]
|
)
|
||||||
|
return fields
|
||||||
|
|
||||||
def normalize_source_object(self, animal):
|
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 {
|
return {
|
||||||
"uuid": animal.farmos_uuid or self.app.make_true_uuid(),
|
"uuid": term.farmos_uuid or self.app.make_true_uuid(),
|
||||||
"asset_name": animal.asset_name,
|
"name": term.name,
|
||||||
"animal_type_uuid": animal.animal_type.farmos_uuid,
|
"description": term.description,
|
||||||
"sex": animal.sex,
|
"_src_object": term,
|
||||||
"is_sterile": animal.is_sterile,
|
|
||||||
"produces_eggs": animal.produces_eggs,
|
|
||||||
"birthdate": animal.birthdate,
|
|
||||||
"notes": animal.notes,
|
|
||||||
"archived": animal.archived,
|
|
||||||
"_src_object": animal,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
WuttaFarm → farmOS API exporter for Animal Types
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source_model_class = model.AnimalType
|
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):
|
source_model_class = model.MaterialType
|
||||||
return {
|
|
||||||
"uuid": animal_type.farmos_uuid or self.app.make_true_uuid(),
|
|
||||||
"name": animal_type.name,
|
|
||||||
"_src_object": animal_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter):
|
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
|
WuttaFarm → farmOS API exporter for Group Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source_model_class = model.GroupAsset
|
source_model_class = model.GroupAsset
|
||||||
|
|
||||||
supported_fields = [
|
def get_supported_fields(self):
|
||||||
"uuid",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"produces_eggs",
|
[
|
||||||
"notes",
|
"produces_eggs",
|
||||||
"archived",
|
]
|
||||||
]
|
)
|
||||||
|
return fields
|
||||||
|
|
||||||
def normalize_source_object(self, group):
|
def normalize_source_object(self, group):
|
||||||
return {
|
data = super().normalize_source_object(group)
|
||||||
"uuid": group.farmos_uuid or self.app.make_true_uuid(),
|
data.update(
|
||||||
"asset_name": group.asset_name,
|
{
|
||||||
"produces_eggs": group.produces_eggs,
|
"produces_eggs": group.produces_eggs,
|
||||||
"notes": group.notes,
|
}
|
||||||
"archived": group.archived,
|
)
|
||||||
"_src_object": group,
|
return data
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter):
|
class LandAssetImporter(FromWuttaFarmAsset, farmos_importing.model.LandAssetImporter):
|
||||||
"""
|
"""
|
||||||
WuttaFarm → farmOS API exporter for Land Assets
|
WuttaFarm → farmOS API exporter for Land Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source_model_class = model.LandAsset
|
source_model_class = model.LandAsset
|
||||||
|
|
||||||
supported_fields = [
|
def get_supported_fields(self):
|
||||||
"uuid",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"land_type_id",
|
[
|
||||||
"is_location",
|
"land_type_id",
|
||||||
"is_fixed",
|
]
|
||||||
"notes",
|
)
|
||||||
"archived",
|
return fields
|
||||||
]
|
|
||||||
|
|
||||||
def normalize_source_object(self, land):
|
def normalize_source_object(self, land):
|
||||||
return {
|
data = super().normalize_source_object(land)
|
||||||
"uuid": land.farmos_uuid or self.app.make_true_uuid(),
|
data.update(
|
||||||
"asset_name": land.asset_name,
|
{
|
||||||
"land_type_id": land.land_type.drupal_id,
|
"land_type_id": land.land_type.drupal_id,
|
||||||
"is_location": land.is_location,
|
}
|
||||||
"is_fixed": land.is_fixed,
|
)
|
||||||
"notes": land.notes,
|
return data
|
||||||
"archived": land.archived,
|
|
||||||
"_src_object": land,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter):
|
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
|
WuttaFarm → farmOS API exporter for Plant Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source_model_class = model.PlantAsset
|
source_model_class = model.PlantAsset
|
||||||
|
|
||||||
supported_fields = [
|
def get_supported_fields(self):
|
||||||
"uuid",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"plant_type_uuids",
|
[
|
||||||
"notes",
|
"plant_type_uuids",
|
||||||
"archived",
|
"season_uuids",
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
return fields
|
||||||
|
|
||||||
def normalize_source_object(self, plant):
|
def normalize_source_object(self, plant):
|
||||||
return {
|
data = super().normalize_source_object(plant)
|
||||||
"uuid": plant.farmos_uuid or self.app.make_true_uuid(),
|
data.update(
|
||||||
"asset_name": plant.asset_name,
|
{
|
||||||
"plant_type_uuids": [t.plant_type.farmos_uuid for t in plant._plant_types],
|
"plant_type_uuids": [pt.farmos_uuid for pt in plant.plant_types],
|
||||||
"notes": plant.notes,
|
"season_uuids": [s.farmos_uuid for s in plant.seasons],
|
||||||
"archived": plant.archived,
|
}
|
||||||
"_src_object": plant,
|
)
|
||||||
}
|
return data
|
||||||
|
|
||||||
|
|
||||||
class StructureAssetImporter(
|
class StructureAssetImporter(
|
||||||
FromWuttaFarm, farmos_importing.model.StructureAssetImporter
|
FromWuttaFarmAsset, farmos_importing.model.StructureAssetImporter
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
WuttaFarm → farmOS API exporter for Structure Assets
|
WuttaFarm → farmOS API exporter for Structure Assets
|
||||||
|
|
@ -325,27 +448,31 @@ class StructureAssetImporter(
|
||||||
|
|
||||||
source_model_class = model.StructureAsset
|
source_model_class = model.StructureAsset
|
||||||
|
|
||||||
supported_fields = [
|
def get_supported_fields(self):
|
||||||
"uuid",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"structure_type_id",
|
[
|
||||||
"is_location",
|
"structure_type_id",
|
||||||
"is_fixed",
|
]
|
||||||
"notes",
|
)
|
||||||
"archived",
|
return fields
|
||||||
]
|
|
||||||
|
|
||||||
def normalize_source_object(self, structure):
|
def normalize_source_object(self, structure):
|
||||||
return {
|
data = super().normalize_source_object(structure)
|
||||||
"uuid": structure.farmos_uuid or self.app.make_true_uuid(),
|
data.update(
|
||||||
"asset_name": structure.asset_name,
|
{
|
||||||
"structure_type_id": structure.structure_type.drupal_id,
|
"structure_type_id": structure.structure_type.drupal_id,
|
||||||
"is_location": structure.is_location,
|
}
|
||||||
"is_fixed": structure.is_fixed,
|
)
|
||||||
"notes": structure.notes,
|
return data
|
||||||
"archived": structure.archived,
|
|
||||||
"_src_object": structure,
|
|
||||||
}
|
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(
|
class StandardQuantityImporter(
|
||||||
FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter
|
FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter
|
||||||
):
|
):
|
||||||
|
|
@ -411,6 +556,8 @@ class FromWuttaFarmLog(FromWuttaFarm):
|
||||||
"notes",
|
"notes",
|
||||||
"quick",
|
"quick",
|
||||||
"assets",
|
"assets",
|
||||||
|
"locations",
|
||||||
|
"groups",
|
||||||
"quantities",
|
"quantities",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -425,6 +572,8 @@ class FromWuttaFarmLog(FromWuttaFarm):
|
||||||
"notes": log.notes,
|
"notes": log.notes,
|
||||||
"quick": self.config.parse_list(log.quick) if log.quick else [],
|
"quick": self.config.parse_list(log.quick) if log.quick else [],
|
||||||
"assets": [(a.asset_type, a.farmos_uuid) for a in log.assets],
|
"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],
|
"quantities": [qty.farmos_uuid for qty in log.quantities],
|
||||||
"_src_object": log,
|
"_src_object": log,
|
||||||
}
|
}
|
||||||
|
|
@ -480,3 +629,22 @@ class ObservationLogImporter(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source_model_class = model.ObservationLog
|
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["LandAsset"] = LandAssetImporter
|
||||||
importers["StructureType"] = StructureTypeImporter
|
importers["StructureType"] = StructureTypeImporter
|
||||||
importers["StructureAsset"] = StructureAssetImporter
|
importers["StructureAsset"] = StructureAssetImporter
|
||||||
|
importers["WaterAsset"] = WaterAssetImporter
|
||||||
|
importers["EquipmentType"] = EquipmentTypeImporter
|
||||||
|
importers["EquipmentAsset"] = EquipmentAssetImporter
|
||||||
importers["AnimalType"] = AnimalTypeImporter
|
importers["AnimalType"] = AnimalTypeImporter
|
||||||
importers["AnimalAsset"] = AnimalAssetImporter
|
importers["AnimalAsset"] = AnimalAssetImporter
|
||||||
importers["GroupAsset"] = GroupAssetImporter
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
importers["PlantType"] = PlantTypeImporter
|
importers["PlantType"] = PlantTypeImporter
|
||||||
|
importers["Season"] = SeasonImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
importers["PlantAsset"] = PlantAssetImporter
|
||||||
importers["Measure"] = MeasureImporter
|
importers["Measure"] = MeasureImporter
|
||||||
importers["Unit"] = UnitImporter
|
importers["Unit"] = UnitImporter
|
||||||
|
importers["MaterialType"] = MaterialTypeImporter
|
||||||
importers["QuantityType"] = QuantityTypeImporter
|
importers["QuantityType"] = QuantityTypeImporter
|
||||||
importers["StandardQuantity"] = StandardQuantityImporter
|
importers["StandardQuantity"] = StandardQuantityImporter
|
||||||
|
importers["MaterialQuantity"] = MaterialQuantityImporter
|
||||||
importers["LogType"] = LogTypeImporter
|
importers["LogType"] = LogTypeImporter
|
||||||
importers["ActivityLog"] = ActivityLogImporter
|
importers["ActivityLog"] = ActivityLogImporter
|
||||||
importers["HarvestLog"] = HarvestLogImporter
|
importers["HarvestLog"] = HarvestLogImporter
|
||||||
importers["MedicalLog"] = MedicalLogImporter
|
importers["MedicalLog"] = MedicalLogImporter
|
||||||
importers["ObservationLog"] = ObservationLogImporter
|
importers["ObservationLog"] = ObservationLogImporter
|
||||||
|
importers["SeedingLog"] = SeedingLogImporter
|
||||||
return importers
|
return importers
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -149,6 +156,8 @@ class FromFarmOS(Importer):
|
||||||
|
|
||||||
:returns: Equivalent naive UTC ``datetime``
|
:returns: Equivalent naive UTC ``datetime``
|
||||||
"""
|
"""
|
||||||
|
if not dt:
|
||||||
|
return None
|
||||||
dt = datetime.datetime.fromisoformat(dt)
|
dt = datetime.datetime.fromisoformat(dt)
|
||||||
return self.app.make_utc(dt)
|
return self.app.make_utc(dt)
|
||||||
|
|
||||||
|
|
@ -330,21 +339,20 @@ class AnimalAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
model_class = model.AnimalAsset
|
model_class = model.AnimalAsset
|
||||||
|
|
||||||
supported_fields = [
|
animal_types_by_farmos_uuid = None
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
def get_supported_fields(self):
|
||||||
"asset_type",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"animal_type_uuid",
|
[
|
||||||
"sex",
|
"animal_type_uuid",
|
||||||
"is_sterile",
|
"sex",
|
||||||
"produces_eggs",
|
"is_sterile",
|
||||||
"birthdate",
|
"produces_eggs",
|
||||||
"notes",
|
"birthdate",
|
||||||
"archived",
|
]
|
||||||
"image_url",
|
)
|
||||||
"thumbnail_url",
|
return fields
|
||||||
]
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
super().setup()
|
super().setup()
|
||||||
|
|
@ -355,6 +363,17 @@ class AnimalAssetImporter(AssetImporterBase):
|
||||||
if animal_type.farmos_uuid:
|
if animal_type.farmos_uuid:
|
||||||
self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type
|
self.animal_types_by_farmos_uuid[animal_type.farmos_uuid] = animal_type
|
||||||
|
|
||||||
|
def get_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):
|
def normalize_source_object(self, animal):
|
||||||
""" """
|
""" """
|
||||||
animal_type_uuid = None
|
animal_type_uuid = None
|
||||||
|
|
@ -362,7 +381,7 @@ class AnimalAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
if animal_type := relationships.get("animal_type"):
|
if animal_type := relationships.get("animal_type"):
|
||||||
if animal_type["data"]:
|
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"])
|
UUID(animal_type["data"]["id"])
|
||||||
):
|
):
|
||||||
animal_type_uuid = wf_animal_type.uuid
|
animal_type_uuid = wf_animal_type.uuid
|
||||||
|
|
@ -399,12 +418,12 @@ class AnimalAssetImporter(AssetImporterBase):
|
||||||
return data
|
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 = [
|
supported_fields = [
|
||||||
"farmos_uuid",
|
"farmos_uuid",
|
||||||
|
|
@ -415,19 +434,50 @@ class AnimalTypeImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
animal_types = self.farmos_client.resource.get("taxonomy_term", "animal_type")
|
return list(
|
||||||
return animal_types["data"]
|
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 {
|
return {
|
||||||
"farmos_uuid": UUID(animal_type["id"]),
|
"farmos_uuid": UUID(term["id"]),
|
||||||
"drupal_id": animal_type["attributes"]["drupal_internal__tid"],
|
"drupal_id": term["attributes"]["drupal_internal__tid"],
|
||||||
"name": animal_type["attributes"]["name"],
|
"name": term["attributes"]["name"],
|
||||||
"description": animal_type["attributes"]["description"],
|
"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):
|
class AssetTypeImporter(FromFarmOS, ToWutta):
|
||||||
"""
|
"""
|
||||||
farmOS API → WuttaFarm importer for Asset Types
|
farmOS API → WuttaFarm importer for Asset Types
|
||||||
|
|
@ -444,8 +494,7 @@ class AssetTypeImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
asset_types = self.farmos_client.resource.get("asset_type")
|
return list(self.farmos_client.resource.iterate("asset_type"))
|
||||||
return asset_types["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, 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):
|
class GroupAssetImporter(AssetImporterBase):
|
||||||
"""
|
"""
|
||||||
farmOS API → WuttaFarm importer for Group Assets
|
farmOS API → WuttaFarm importer for Group Assets
|
||||||
|
|
@ -464,20 +631,14 @@ class GroupAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
model_class = model.GroupAsset
|
model_class = model.GroupAsset
|
||||||
|
|
||||||
supported_fields = [
|
def get_supported_fields(self):
|
||||||
"farmos_uuid",
|
fields = list(super().get_supported_fields())
|
||||||
"drupal_id",
|
fields.extend(
|
||||||
"asset_type",
|
[
|
||||||
"asset_name",
|
"produces_eggs",
|
||||||
"is_location",
|
]
|
||||||
"is_fixed",
|
)
|
||||||
"produces_eggs",
|
return fields
|
||||||
"notes",
|
|
||||||
"archived",
|
|
||||||
"image_url",
|
|
||||||
"thumbnail_url",
|
|
||||||
"parents",
|
|
||||||
]
|
|
||||||
|
|
||||||
def normalize_source_object(self, group):
|
def normalize_source_object(self, group):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -497,18 +658,16 @@ class LandAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
model_class = model.LandAsset
|
model_class = model.LandAsset
|
||||||
|
|
||||||
supported_fields = [
|
land_types_by_id = None
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
def get_supported_fields(self):
|
||||||
"asset_type",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"land_type_uuid",
|
[
|
||||||
"is_location",
|
"land_type_uuid",
|
||||||
"is_fixed",
|
]
|
||||||
"notes",
|
)
|
||||||
"archived",
|
return fields
|
||||||
"parents",
|
|
||||||
]
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -519,10 +678,21 @@ class LandAssetImporter(AssetImporterBase):
|
||||||
for land_type in self.target_session.query(model.LandType):
|
for land_type in self.target_session.query(model.LandType):
|
||||||
self.land_types_by_id[land_type.drupal_id] = land_type
|
self.land_types_by_id[land_type.drupal_id] = land_type
|
||||||
|
|
||||||
|
def get_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):
|
def normalize_source_object(self, land):
|
||||||
""" """
|
""" """
|
||||||
land_type_id = land["attributes"]["land_type"]
|
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:
|
if not land_type:
|
||||||
log.warning(
|
log.warning(
|
||||||
"invalid land_type '%s' for farmOS Land Asset: %s", land_type_id, land
|
"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):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
land_types = self.farmos_client.resource.get("land_type")
|
return list(self.farmos_client.resource.iterate("land_type"))
|
||||||
return land_types["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, land_type):
|
def normalize_source_object(self, land_type):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -581,8 +750,7 @@ class PlantTypeImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
result = self.farmos_client.resource.get("taxonomy_term", "plant_type")
|
return list(self.farmos_client.resource.iterate("taxonomy_term", "plant_type"))
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, 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):
|
class PlantAssetImporter(AssetImporterBase):
|
||||||
"""
|
"""
|
||||||
farmOS API → WuttaFarm importer for Plant Assets
|
farmOS API → WuttaFarm importer for Plant Assets
|
||||||
|
|
@ -601,17 +797,18 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
model_class = model.PlantAsset
|
model_class = model.PlantAsset
|
||||||
|
|
||||||
supported_fields = [
|
plant_types_by_farmos_uuid = None
|
||||||
"farmos_uuid",
|
seasons_by_farmos_uuid = None
|
||||||
"drupal_id",
|
|
||||||
"asset_type",
|
def get_supported_fields(self):
|
||||||
"asset_name",
|
fields = list(super().get_supported_fields())
|
||||||
"plant_types",
|
fields.extend(
|
||||||
"notes",
|
[
|
||||||
"archived",
|
"plant_types",
|
||||||
"image_url",
|
"seasons",
|
||||||
"thumbnail_url",
|
]
|
||||||
]
|
)
|
||||||
|
return fields
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
super().setup()
|
super().setup()
|
||||||
|
|
@ -622,25 +819,61 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
if plant_type.farmos_uuid:
|
if plant_type.farmos_uuid:
|
||||||
self.plant_types_by_farmos_uuid[plant_type.farmos_uuid] = plant_type
|
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):
|
def normalize_source_object(self, plant):
|
||||||
""" """
|
""" """
|
||||||
|
data = super().normalize_source_object(plant)
|
||||||
|
|
||||||
plant_types = []
|
plant_types = []
|
||||||
|
seasons = []
|
||||||
if relationships := plant.get("relationships"):
|
if relationships := plant.get("relationships"):
|
||||||
|
|
||||||
if plant_type := relationships.get("plant_type"):
|
if plant_type := relationships.get("plant_type"):
|
||||||
plant_types = []
|
plant_types = []
|
||||||
for plant_type in plant_type["data"]:
|
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"])
|
UUID(plant_type["id"])
|
||||||
):
|
):
|
||||||
plant_types.append(wf_plant_type.uuid)
|
plant_types.append(wf_plant_type.uuid)
|
||||||
else:
|
else:
|
||||||
log.warning("plant type not found: %s", plant_type["id"])
|
log.warning("plant type not found: %s", plant_type["id"])
|
||||||
|
|
||||||
data = super().normalize_source_object(plant)
|
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(
|
data.update(
|
||||||
{
|
{
|
||||||
"plant_types": set(plant_types),
|
"plant_types": set(plant_types),
|
||||||
|
"seasons": set(seasons),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
@ -651,6 +884,9 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
if "plant_types" in self.fields:
|
if "plant_types" in self.fields:
|
||||||
data["plant_types"] = set([pt.uuid for pt in plant.plant_types])
|
data["plant_types"] = 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
|
return data
|
||||||
|
|
||||||
def update_target_object(self, plant, source_data, target_data=None):
|
def update_target_object(self, plant, source_data, target_data=None):
|
||||||
|
|
@ -683,6 +919,25 @@ class PlantAssetImporter(AssetImporterBase):
|
||||||
)
|
)
|
||||||
self.target_session.delete(plant_type)
|
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
|
return plant
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -693,20 +948,16 @@ class StructureAssetImporter(AssetImporterBase):
|
||||||
|
|
||||||
model_class = model.StructureAsset
|
model_class = model.StructureAsset
|
||||||
|
|
||||||
supported_fields = [
|
structure_types_by_id = None
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
def get_supported_fields(self):
|
||||||
"asset_type",
|
fields = list(super().get_supported_fields())
|
||||||
"asset_name",
|
fields.extend(
|
||||||
"structure_type_uuid",
|
[
|
||||||
"is_location",
|
"structure_type_uuid",
|
||||||
"is_fixed",
|
]
|
||||||
"notes",
|
)
|
||||||
"archived",
|
return fields
|
||||||
"image_url",
|
|
||||||
"thumbnail_url",
|
|
||||||
"parents",
|
|
||||||
]
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
super().setup()
|
super().setup()
|
||||||
|
|
@ -716,10 +967,21 @@ class StructureAssetImporter(AssetImporterBase):
|
||||||
for structure_type in self.target_session.query(model.StructureType):
|
for structure_type in self.target_session.query(model.StructureType):
|
||||||
self.structure_types_by_id[structure_type.drupal_id] = structure_type
|
self.structure_types_by_id[structure_type.drupal_id] = structure_type
|
||||||
|
|
||||||
|
def get_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):
|
def normalize_source_object(self, structure):
|
||||||
""" """
|
""" """
|
||||||
structure_type_id = structure["attributes"]["structure_type"]
|
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:
|
if not structure_type:
|
||||||
log.warning(
|
log.warning(
|
||||||
"invalid structure_type '%s' for farmOS Structure Asset: %s",
|
"invalid structure_type '%s' for farmOS Structure Asset: %s",
|
||||||
|
|
@ -752,8 +1014,7 @@ class StructureTypeImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
structure_types = self.farmos_client.resource.get("structure_type")
|
return list(self.farmos_client.resource.iterate("structure_type"))
|
||||||
return structure_types["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, 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):
|
class UserImporter(FromFarmOS, ToWutta):
|
||||||
"""
|
"""
|
||||||
farmOS API → WuttaFarm importer for Users
|
farmOS API → WuttaFarm importer for Users
|
||||||
|
|
@ -791,8 +1060,7 @@ class UserImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
users = self.farmos_client.resource.get("user")
|
return list(self.farmos_client.resource.iterate("user"))
|
||||||
return users["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, user):
|
def normalize_source_object(self, user):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -833,6 +1101,7 @@ class MeasureImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
supported_fields = [
|
supported_fields = [
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
|
"ordinal",
|
||||||
"name",
|
"name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -843,12 +1112,15 @@ class MeasureImporter(FromFarmOS, ToWutta):
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
self.ordinal = 0
|
||||||
return data["definitions"]["attributes"]["properties"]["measure"]["oneOf"]
|
return data["definitions"]["attributes"]["properties"]["measure"]["oneOf"]
|
||||||
|
|
||||||
def normalize_source_object(self, measure):
|
def normalize_source_object(self, measure):
|
||||||
""" """
|
""" """
|
||||||
|
self.ordinal += 1
|
||||||
return {
|
return {
|
||||||
"drupal_id": measure["const"],
|
"drupal_id": measure["const"],
|
||||||
|
"ordinal": self.ordinal,
|
||||||
"name": measure["title"],
|
"name": measure["title"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -869,8 +1141,7 @@ class UnitImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
result = self.farmos_client.resource.get("taxonomy_term", "unit")
|
return list(self.farmos_client.resource.iterate("taxonomy_term", "unit"))
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, unit):
|
def normalize_source_object(self, unit):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -898,8 +1169,7 @@ class QuantityTypeImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
result = self.farmos_client.resource.get("quantity_type")
|
return list(self.farmos_client.resource.iterate("quantity_type"))
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, quantity_type):
|
def normalize_source_object(self, quantity_type):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -927,8 +1197,7 @@ class LogTypeImporter(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
log_types = self.farmos_client.resource.get("log_type")
|
return list(self.farmos_client.resource.iterate("log_type"))
|
||||||
return log_types["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, log_type):
|
def normalize_source_object(self, log_type):
|
||||||
""" """
|
""" """
|
||||||
|
|
@ -1226,6 +1495,41 @@ class ObservationLogImporter(LogImporterBase):
|
||||||
model_class = model.ObservationLog
|
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):
|
class QuantityImporterBase(FromFarmOS, ToWutta):
|
||||||
"""
|
"""
|
||||||
Base class for farmOS API → WuttaFarm quantity importers
|
Base class for farmOS API → WuttaFarm quantity importers
|
||||||
|
|
@ -1271,8 +1575,7 @@ class QuantityImporterBase(FromFarmOS, ToWutta):
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
quantity_type = self.get_farmos_quantity_type()
|
quantity_type = self.get_farmos_quantity_type()
|
||||||
result = self.farmos_client.resource.get("quantity", quantity_type)
|
return list(self.farmos_client.resource.iterate("quantity", quantity_type))
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def get_quantity_type_by_farmos_uuid(self, uuid):
|
def get_quantity_type_by_farmos_uuid(self, uuid):
|
||||||
if hasattr(self, "quantity_types_by_farmos_uuid"):
|
if hasattr(self, "quantity_types_by_farmos_uuid"):
|
||||||
|
|
@ -1355,3 +1658,76 @@ class StandardQuantityImporter(QuantityImporterBase):
|
||||||
"units_uuid",
|
"units_uuid",
|
||||||
"label",
|
"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
|
self._farmos_units = units
|
||||||
return self._farmos_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={}):
|
def normalize_farmos_asset(self, asset, included={}):
|
||||||
""" """
|
""" """
|
||||||
|
|
||||||
if notes := asset["attributes"]["notes"]:
|
if notes := asset["attributes"]["notes"]:
|
||||||
notes = notes["value"]
|
notes = notes["value"]
|
||||||
|
|
||||||
|
parent_objects = []
|
||||||
|
parent_uuids = []
|
||||||
owner_objects = []
|
owner_objects = []
|
||||||
owner_uuids = []
|
owner_uuids = []
|
||||||
if relationships := asset.get("relationships"):
|
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"):
|
if owners := relationships.get("owner"):
|
||||||
for user in owners["data"]:
|
for user in owners["data"]:
|
||||||
user_uuid = user["id"]
|
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 {
|
return {
|
||||||
"uuid": asset["id"],
|
"uuid": asset["id"],
|
||||||
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
||||||
|
|
@ -114,6 +144,10 @@ class Normalizer(GenericHandler):
|
||||||
"is_fixed": asset["attributes"]["is_fixed"],
|
"is_fixed": asset["attributes"]["is_fixed"],
|
||||||
"archived": asset["attributes"]["archived"],
|
"archived": asset["attributes"]["archived"],
|
||||||
"notes": notes,
|
"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,
|
"owners": owner_objects,
|
||||||
"owner_uuids": owner_uuids,
|
"owner_uuids": owner_uuids,
|
||||||
}
|
}
|
||||||
|
|
@ -121,8 +155,7 @@ class Normalizer(GenericHandler):
|
||||||
def normalize_farmos_log(self, log, included={}):
|
def normalize_farmos_log(self, log, included={}):
|
||||||
|
|
||||||
if timestamp := log["attributes"]["timestamp"]:
|
if timestamp := log["attributes"]["timestamp"]:
|
||||||
timestamp = datetime.datetime.fromisoformat(timestamp)
|
timestamp = self.normalize_datetime(timestamp)
|
||||||
timestamp = self.app.localtime(timestamp)
|
|
||||||
|
|
||||||
if notes := log["attributes"]["notes"]:
|
if notes := log["attributes"]["notes"]:
|
||||||
notes = notes["value"]
|
notes = notes["value"]
|
||||||
|
|
@ -232,27 +265,29 @@ class Normalizer(GenericHandler):
|
||||||
|
|
||||||
measure_id = attrs["measure"]
|
measure_id = attrs["measure"]
|
||||||
|
|
||||||
quantity_objects.append(
|
quantity_object = {
|
||||||
{
|
"uuid": quantity["id"],
|
||||||
"uuid": quantity["id"],
|
"drupal_id": attrs["drupal_internal__id"],
|
||||||
"drupal_id": attrs["drupal_internal__id"],
|
"quantity_type_uuid": rels["quantity_type"]["data"]["id"],
|
||||||
"quantity_type_uuid": rels["quantity_type"]["data"][
|
"quantity_type_id": rels["quantity_type"]["data"]["meta"][
|
||||||
"id"
|
"drupal_internal__target_id"
|
||||||
],
|
],
|
||||||
"quantity_type_id": rels["quantity_type"]["data"][
|
"measure_id": measure_id,
|
||||||
"meta"
|
"measure_name": self.get_farmos_measure_name(measure_id),
|
||||||
]["drupal_internal__target_id"],
|
"value_numerator": value["numerator"],
|
||||||
"measure_id": measure_id,
|
"value_decimal": value["decimal"],
|
||||||
"measure_name": self.get_farmos_measure_name(
|
"value_denominator": value["denominator"],
|
||||||
measure_id
|
"unit_uuid": unit_uuid,
|
||||||
),
|
"unit_name": unit["attributes"]["name"],
|
||||||
"value_numerator": value["numerator"],
|
}
|
||||||
"value_decimal": value["decimal"],
|
if quantity_object["quantity_type_id"] == "material":
|
||||||
"value_denominator": value["denominator"],
|
quantity_object["material_types"] = [
|
||||||
"unit_uuid": unit_uuid,
|
{"uuid": mtype["id"]}
|
||||||
"unit_name": unit["attributes"]["name"],
|
for mtype in quantity["relationships"]["material_type"][
|
||||||
}
|
"data"
|
||||||
)
|
]
|
||||||
|
]
|
||||||
|
quantity_objects.append(quantity_object)
|
||||||
|
|
||||||
if owners := relationships.get("owner"):
|
if owners := relationships.get("owner"):
|
||||||
for user in owners["data"]:
|
for user in owners["data"]:
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import json
|
||||||
import colander
|
import colander
|
||||||
|
|
||||||
from wuttaweb.db import Session
|
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
|
from wuttaweb.forms.widgets import NotesWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -164,10 +164,9 @@ class FarmOSRefs(WuttaSet):
|
||||||
self.route_prefix = route_prefix
|
self.route_prefix = route_prefix
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
def serialize(self, node, appstruct):
|
||||||
if appstruct is colander.null:
|
if not appstruct:
|
||||||
return colander.null
|
return colander.null
|
||||||
|
return appstruct
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
def widget_maker(self, **kwargs):
|
||||||
from wuttafarm.web.forms.widgets import FarmOSRefsWidget
|
from wuttafarm.web.forms.widgets import FarmOSRefsWidget
|
||||||
|
|
@ -217,6 +216,35 @@ class FarmOSQuantityRefs(WuttaSet):
|
||||||
return FarmOSQuantityRefsWidget(**kwargs)
|
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):
|
class FarmOSPlantTypes(colander.SchemaType):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
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)
|
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):
|
class PlantTypeRefs(WuttaSet):
|
||||||
"""
|
"""
|
||||||
Schema type for Plant Types field (on a Plant Asset).
|
Schema type for Plant Types field (on a Plant Asset).
|
||||||
|
|
@ -288,6 +345,62 @@ class PlantTypeRefs(WuttaSet):
|
||||||
return PlantTypeRefsWidget(self.request, **kwargs)
|
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):
|
class StructureType(colander.SchemaType):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
|
@ -372,55 +485,100 @@ class UsersType(colander.SchemaType):
|
||||||
return UsersWidget(self.request, **kwargs)
|
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):
|
class AssetRefs(WuttaSet):
|
||||||
"""
|
"""
|
||||||
Schema type for Assets field (on a Log record)
|
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):
|
def serialize(self, node, appstruct):
|
||||||
if not appstruct:
|
if not appstruct:
|
||||||
return colander.null
|
return colander.null
|
||||||
|
|
||||||
return {asset.uuid for asset in appstruct}
|
return {asset.uuid.hex for asset in appstruct}
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
def widget_maker(self, **kwargs):
|
||||||
from wuttafarm.web.forms.widgets import AssetRefsWidget
|
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)
|
return AssetRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LogQuantityRefs(WuttaSet):
|
class QuantityRefs(colander.List):
|
||||||
"""
|
"""
|
||||||
Schema type for Quantities field (on a Log record)
|
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):
|
def serialize(self, node, appstruct):
|
||||||
if not appstruct:
|
if not appstruct:
|
||||||
return colander.null
|
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):
|
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):
|
class OwnerRefs(WuttaSet):
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.util import render_quantity_objects
|
from wuttafarm.web.util import render_quantity_objects
|
||||||
|
from wuttafarm.db.model import EquipmentType
|
||||||
|
|
||||||
|
|
||||||
class ImageWidget(Widget):
|
class ImageWidget(Widget):
|
||||||
|
|
@ -124,7 +125,7 @@ class FarmOSRefsWidget(Widget):
|
||||||
return HTML.tag("span")
|
return HTML.tag("span")
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
for obj in json.loads(cstruct):
|
for obj in cstruct:
|
||||||
url = self.request.route_url(
|
url = self.request.route_url(
|
||||||
f"{self.route_prefix}.view", uuid=obj["uuid"]
|
f"{self.route_prefix}.view", uuid=obj["uuid"]
|
||||||
)
|
)
|
||||||
|
|
@ -228,6 +229,38 @@ class FarmOSUnitRefWidget(Widget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
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):
|
class FarmOSPlantTypesWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget to display a farmOS "plant types" field.
|
Widget to display a farmOS "plant types" field.
|
||||||
|
|
@ -258,6 +291,88 @@ class FarmOSPlantTypesWidget(Widget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
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):
|
class PlantTypeRefsWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget for Plant Types field (on a Plant Asset).
|
Widget for Plant Types field (on a Plant Asset).
|
||||||
|
|
@ -332,6 +447,144 @@ class PlantTypeRefsWidget(Widget):
|
||||||
return set(pstruct.split(","))
|
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):
|
class StructureWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget to display a "structure" field.
|
Widget to display a "structure" field.
|
||||||
|
|
@ -393,42 +646,20 @@ class UsersWidget(Widget):
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
|
||||||
class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
|
class AssetRefsWidget(Widget):
|
||||||
"""
|
|
||||||
Widget for Parents field which references assets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
|
||||||
""" """
|
|
||||||
model = self.app.model
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
|
||||||
if readonly:
|
|
||||||
parents = []
|
|
||||||
for uuid in json.loads(cstruct):
|
|
||||||
parent = session.get(model.Asset, uuid)
|
|
||||||
parents.append(
|
|
||||||
HTML.tag(
|
|
||||||
"li",
|
|
||||||
c=tags.link_to(
|
|
||||||
str(parent),
|
|
||||||
self.request.route_url(
|
|
||||||
f"{parent.asset_type}_assets.view", uuid=parent.uuid
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return HTML.tag("ul", c=parents)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRefsWidget(WuttaCheckboxChoiceWidget):
|
|
||||||
"""
|
"""
|
||||||
Widget for Assets field (of various kinds).
|
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):
|
def serialize(self, field, cstruct, **kw):
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
@ -452,14 +683,43 @@ class AssetRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
)
|
)
|
||||||
return HTML.tag("ul", c=assets)
|
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)
|
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):
|
def serialize(self, field, cstruct, **kw):
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
@ -467,24 +727,78 @@ class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
readonly = kw.get("readonly", self.readonly)
|
||||||
if readonly:
|
if readonly:
|
||||||
|
if not cstruct:
|
||||||
|
return ""
|
||||||
|
|
||||||
quantities = []
|
quantities = []
|
||||||
for uuid in cstruct or []:
|
for qty in cstruct:
|
||||||
qty = session.get(model.Quantity, uuid)
|
url = self.request.route_url(
|
||||||
quantities.append(
|
f"quantities_{qty['quantity_type']['drupal_id']}.view",
|
||||||
HTML.tag(
|
uuid=qty["uuid"],
|
||||||
"li",
|
|
||||||
c=tags.link_to(
|
|
||||||
qty.render_as_text(self.config),
|
|
||||||
# TODO
|
|
||||||
self.request.route_url(
|
|
||||||
"quantities_standard.view", uuid=qty.uuid
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
quantities.append(HTML.tag("li", c=tags.link_to(qty["as_text"], url)))
|
||||||
|
|
||||||
return HTML.tag("ul", c=quantities)
|
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):
|
class OwnerRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "animal_assets",
|
"route": "animal_assets",
|
||||||
"perm": "animal_assets.list",
|
"perm": "animal_assets.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Equipment",
|
||||||
|
"route": "equipment_assets",
|
||||||
|
"perm": "equipment_assets.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Group",
|
"title": "Group",
|
||||||
"route": "group_assets",
|
"route": "group_assets",
|
||||||
|
|
@ -112,12 +117,22 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "structure_assets",
|
"route": "structure_assets",
|
||||||
"perm": "structure_assets.list",
|
"perm": "structure_assets.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Water",
|
||||||
|
"route": "water_assets",
|
||||||
|
"perm": "water_assets.list",
|
||||||
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Animal Types",
|
"title": "Animal Types",
|
||||||
"route": "animal_types",
|
"route": "animal_types",
|
||||||
"perm": "animal_types.list",
|
"perm": "animal_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Equipment Types",
|
||||||
|
"route": "equipment_types",
|
||||||
|
"perm": "equipment_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Land Types",
|
"title": "Land Types",
|
||||||
"route": "land_types",
|
"route": "land_types",
|
||||||
|
|
@ -128,6 +143,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "plant_types",
|
"route": "plant_types",
|
||||||
"perm": "plant_types.list",
|
"perm": "plant_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seasons",
|
||||||
|
"route": "seasons",
|
||||||
|
"perm": "seasons.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Structure Types",
|
"title": "Structure Types",
|
||||||
"route": "structure_types",
|
"route": "structure_types",
|
||||||
|
|
@ -171,12 +191,22 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "logs_observation",
|
"route": "logs_observation",
|
||||||
"perm": "logs_observation.list",
|
"perm": "logs_observation.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seeding",
|
||||||
|
"route": "logs_seeding",
|
||||||
|
"perm": "logs_seeding.list",
|
||||||
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "All Quantities",
|
"title": "All Quantities",
|
||||||
"route": "quantities",
|
"route": "quantities",
|
||||||
"perm": "quantities.list",
|
"perm": "quantities.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Quantities",
|
||||||
|
"route": "quantities_material",
|
||||||
|
"perm": "quantities_material.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Standard Quantities",
|
"title": "Standard Quantities",
|
||||||
"route": "quantities_standard",
|
"route": "quantities_standard",
|
||||||
|
|
@ -188,6 +218,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "log_types",
|
"route": "log_types",
|
||||||
"perm": "log_types.list",
|
"perm": "log_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Types",
|
||||||
|
"route": "material_types",
|
||||||
|
"perm": "material_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Measures",
|
"title": "Measures",
|
||||||
"route": "measures",
|
"route": "measures",
|
||||||
|
|
@ -224,6 +259,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_animal_assets",
|
"route": "farmos_animal_assets",
|
||||||
"perm": "farmos_animal_assets.list",
|
"perm": "farmos_animal_assets.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Equipment Assets",
|
||||||
|
"route": "farmos_equipment_assets",
|
||||||
|
"perm": "farmos_equipment_assets.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Group Assets",
|
"title": "Group Assets",
|
||||||
"route": "farmos_group_assets",
|
"route": "farmos_group_assets",
|
||||||
|
|
@ -244,6 +284,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_structure_assets",
|
"route": "farmos_structure_assets",
|
||||||
"perm": "farmos_structure_assets.list",
|
"perm": "farmos_structure_assets.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Water Assets",
|
||||||
|
"route": "farmos_water_assets",
|
||||||
|
"perm": "farmos_water_assets.list",
|
||||||
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Activity Logs",
|
"title": "Activity Logs",
|
||||||
|
|
@ -265,12 +310,22 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_logs_observation",
|
"route": "farmos_logs_observation",
|
||||||
"perm": "farmos_logs_observation.list",
|
"perm": "farmos_logs_observation.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seeding Logs",
|
||||||
|
"route": "farmos_logs_seeding",
|
||||||
|
"perm": "farmos_logs_seeding.list",
|
||||||
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Animal Types",
|
"title": "Animal Types",
|
||||||
"route": "farmos_animal_types",
|
"route": "farmos_animal_types",
|
||||||
"perm": "farmos_animal_types.list",
|
"perm": "farmos_animal_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Equipment Types",
|
||||||
|
"route": "farmos_equipment_types",
|
||||||
|
"perm": "farmos_equipment_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Land Types",
|
"title": "Land Types",
|
||||||
"route": "farmos_land_types",
|
"route": "farmos_land_types",
|
||||||
|
|
@ -281,6 +336,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_plant_types",
|
"route": "farmos_plant_types",
|
||||||
"perm": "farmos_plant_types.list",
|
"perm": "farmos_plant_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seasons",
|
||||||
|
"route": "farmos_seasons",
|
||||||
|
"perm": "farmos_seasons.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Structure Types",
|
"title": "Structure Types",
|
||||||
"route": "farmos_structure_types",
|
"route": "farmos_structure_types",
|
||||||
|
|
@ -297,11 +357,21 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_log_types",
|
"route": "farmos_log_types",
|
||||||
"perm": "farmos_log_types.list",
|
"perm": "farmos_log_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Types",
|
||||||
|
"route": "farmos_material_types",
|
||||||
|
"perm": "farmos_material_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Quantity Types",
|
"title": "Quantity Types",
|
||||||
"route": "farmos_quantity_types",
|
"route": "farmos_quantity_types",
|
||||||
"perm": "farmos_quantity_types.list",
|
"perm": "farmos_quantity_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Quantities",
|
||||||
|
"route": "farmos_quantities_material",
|
||||||
|
"perm": "farmos_quantities_material.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Standard Quantities",
|
"title": "Standard Quantities",
|
||||||
"route": "farmos_quantities_standard",
|
"route": "farmos_quantities_standard",
|
||||||
|
|
@ -333,6 +403,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_animal_assets",
|
"route": "farmos_animal_assets",
|
||||||
"perm": "farmos_animal_assets.list",
|
"perm": "farmos_animal_assets.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Equipment",
|
||||||
|
"route": "farmos_equipment_assets",
|
||||||
|
"perm": "farmos_equipment_assets.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Group",
|
"title": "Group",
|
||||||
"route": "farmos_group_assets",
|
"route": "farmos_group_assets",
|
||||||
|
|
@ -353,12 +428,22 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_structure_assets",
|
"route": "farmos_structure_assets",
|
||||||
"perm": "farmos_structure_assets.list",
|
"perm": "farmos_structure_assets.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Water",
|
||||||
|
"route": "farmos_water_assets",
|
||||||
|
"perm": "farmos_water_assets.list",
|
||||||
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Animal Types",
|
"title": "Animal Types",
|
||||||
"route": "farmos_animal_types",
|
"route": "farmos_animal_types",
|
||||||
"perm": "farmos_animal_types.list",
|
"perm": "farmos_animal_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Equipment Types",
|
||||||
|
"route": "farmos_equipment_types",
|
||||||
|
"perm": "farmos_equipment_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Land Types",
|
"title": "Land Types",
|
||||||
"route": "farmos_land_types",
|
"route": "farmos_land_types",
|
||||||
|
|
@ -369,6 +454,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_plant_types",
|
"route": "farmos_plant_types",
|
||||||
"perm": "farmos_plant_types.list",
|
"perm": "farmos_plant_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seasons",
|
||||||
|
"route": "farmos_seasons",
|
||||||
|
"perm": "farmos_seasons.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Structure Types",
|
"title": "Structure Types",
|
||||||
"route": "farmos_structure_types",
|
"route": "farmos_structure_types",
|
||||||
|
|
@ -410,17 +500,32 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_logs_observation",
|
"route": "farmos_logs_observation",
|
||||||
"perm": "farmos_logs_observation.list",
|
"perm": "farmos_logs_observation.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Seeding",
|
||||||
|
"route": "farmos_logs_seeding",
|
||||||
|
"perm": "farmos_logs_seeding.list",
|
||||||
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Log Types",
|
"title": "Log Types",
|
||||||
"route": "farmos_log_types",
|
"route": "farmos_log_types",
|
||||||
"perm": "farmos_log_types.list",
|
"perm": "farmos_log_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Types",
|
||||||
|
"route": "farmos_material_types",
|
||||||
|
"perm": "farmos_material_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Quantity Types",
|
"title": "Quantity Types",
|
||||||
"route": "farmos_quantity_types",
|
"route": "farmos_quantity_types",
|
||||||
"perm": "farmos_quantity_types.list",
|
"perm": "farmos_quantity_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Quantities",
|
||||||
|
"route": "farmos_quantities_material",
|
||||||
|
"perm": "farmos_quantities_material.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Standard Quantities",
|
"title": "Standard Quantities",
|
||||||
"route": "farmos_quantities_standard",
|
"route": "farmos_quantities_standard",
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</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"
|
<b-checkbox name="${app.appname}.farmos_style_grid_links"
|
||||||
v-model="simpleSettings['${app.appname}.farmos_style_grid_links']"
|
v-model="simpleSettings['${app.appname}.farmos_style_grid_links']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,74 @@
|
||||||
</b-notification>
|
</b-notification>
|
||||||
% endif
|
% 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>
|
</%def>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
<%inherit file="wuttaweb:templates/base.mako" />
|
<%inherit file="wuttaweb:templates/base.mako" />
|
||||||
<%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" />
|
<%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()">
|
<%def name="index_title_controls()">
|
||||||
${parent.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"]
|
measure = quantity["measure_name"]
|
||||||
value = quantity["value_decimal"]
|
value = quantity["value_decimal"]
|
||||||
unit = quantity["unit_name"]
|
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 wuttaweb.views import essential
|
||||||
|
|
||||||
from .master import WuttaFarmMasterView
|
from .master import WuttaFarmMasterView, TaxonomyMasterView
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
@ -48,19 +48,23 @@ def includeme(config):
|
||||||
# native table views
|
# native table views
|
||||||
if mode != enum.FARMOS_INTEGRATION_MODE_WRAPPER:
|
if mode != enum.FARMOS_INTEGRATION_MODE_WRAPPER:
|
||||||
config.include("wuttafarm.web.views.units")
|
config.include("wuttafarm.web.views.units")
|
||||||
|
config.include("wuttafarm.web.views.material_types")
|
||||||
config.include("wuttafarm.web.views.quantities")
|
config.include("wuttafarm.web.views.quantities")
|
||||||
config.include("wuttafarm.web.views.asset_types")
|
config.include("wuttafarm.web.views.asset_types")
|
||||||
config.include("wuttafarm.web.views.assets")
|
config.include("wuttafarm.web.views.assets")
|
||||||
config.include("wuttafarm.web.views.land")
|
config.include("wuttafarm.web.views.land")
|
||||||
config.include("wuttafarm.web.views.structures")
|
config.include("wuttafarm.web.views.structures")
|
||||||
|
config.include("wuttafarm.web.views.equipment")
|
||||||
config.include("wuttafarm.web.views.animals")
|
config.include("wuttafarm.web.views.animals")
|
||||||
config.include("wuttafarm.web.views.groups")
|
config.include("wuttafarm.web.views.groups")
|
||||||
config.include("wuttafarm.web.views.plants")
|
config.include("wuttafarm.web.views.plants")
|
||||||
|
config.include("wuttafarm.web.views.water")
|
||||||
config.include("wuttafarm.web.views.logs")
|
config.include("wuttafarm.web.views.logs")
|
||||||
config.include("wuttafarm.web.views.logs_activity")
|
config.include("wuttafarm.web.views.logs_activity")
|
||||||
config.include("wuttafarm.web.views.logs_harvest")
|
config.include("wuttafarm.web.views.logs_harvest")
|
||||||
config.include("wuttafarm.web.views.logs_medical")
|
config.include("wuttafarm.web.views.logs_medical")
|
||||||
config.include("wuttafarm.web.views.logs_observation")
|
config.include("wuttafarm.web.views.logs_observation")
|
||||||
|
config.include("wuttafarm.web.views.logs_seeding")
|
||||||
|
|
||||||
# quick form views
|
# quick form views
|
||||||
# (nb. these work with all integration modes)
|
# (nb. these work with all integration modes)
|
||||||
|
|
@ -69,3 +73,7 @@ def includeme(config):
|
||||||
# views for farmOS
|
# views for farmOS
|
||||||
if mode != enum.FARMOS_INTEGRATION_MODE_NONE:
|
if mode != enum.FARMOS_INTEGRATION_MODE_NONE:
|
||||||
config.include("wuttafarm.web.views.farmos")
|
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
|
Master view for Animals
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDictEnum
|
from wuttaweb.forms.schema import WuttaDictEnum
|
||||||
|
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
from wuttaweb.util import get_form_data
|
from wuttaweb.util import get_form_data
|
||||||
|
|
||||||
from wuttafarm.db.model import AnimalType, AnimalAsset
|
from wuttafarm.db.model import AnimalType, AnimalAsset
|
||||||
|
|
@ -234,27 +237,6 @@ class AnimalAssetView(AssetMasterView):
|
||||||
"archived",
|
"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):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
@ -288,15 +270,32 @@ class AnimalAssetView(AssetMasterView):
|
||||||
animal = f.model_instance
|
animal = f.model_instance
|
||||||
|
|
||||||
# animal_type
|
# animal_type
|
||||||
|
f.fields.insert_after("asset_name", "animal_type")
|
||||||
f.set_node("animal_type", AnimalTypeRef(self.request))
|
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
|
# sex
|
||||||
|
f.fields.insert_after("produces_eggs", "sex")
|
||||||
if not (self.creating or self.editing) and animal.sex is None:
|
if not (self.creating or self.editing) and animal.sex is None:
|
||||||
pass # TODO: dict enum widget does not handle null values well
|
pass # TODO: dict enum widget does not handle null values well
|
||||||
else:
|
else:
|
||||||
f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX))
|
# 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)
|
f.set_required("sex", False)
|
||||||
|
|
||||||
|
# is_sterile
|
||||||
|
f.fields.insert_after("sex", "is_sterile")
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
Master view for Assets
|
Master view for Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
@ -32,7 +33,7 @@ from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import Asset, Log
|
from wuttafarm.db.model import Asset, Log
|
||||||
from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs
|
from wuttafarm.web.forms.schema import OwnerRefs, AssetRefs
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
from wuttafarm.util import get_log_type_enum
|
from wuttafarm.util import get_log_type_enum
|
||||||
from wuttafarm.web.util import get_farmos_client_for_user
|
from wuttafarm.web.util import get_farmos_client_for_user
|
||||||
|
|
@ -77,6 +78,25 @@ class AssetMasterView(WuttaFarmMasterView):
|
||||||
"archived": {"active": True, "verb": "is_false"},
|
"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
|
has_rows = True
|
||||||
row_model_class = Log
|
row_model_class = Log
|
||||||
rows_viewable = True
|
rows_viewable = True
|
||||||
|
|
@ -261,11 +281,11 @@ class AssetMasterView(WuttaFarmMasterView):
|
||||||
f.set_default("groups", asset_handler.get_groups(asset))
|
f.set_default("groups", asset_handler.get_groups(asset))
|
||||||
|
|
||||||
# parents
|
# parents
|
||||||
if self.creating or self.editing:
|
f.set_node("parents", AssetRefs(self.request, for_asset=asset))
|
||||||
f.remove("parents") # TODO: add support for this
|
f.set_required("parents", False)
|
||||||
else:
|
if not self.creating:
|
||||||
f.set_node("parents", AssetParentRefs(self.request))
|
# nb. must explicity declare value for non-standard field
|
||||||
f.set_default("parents", [p.uuid for p in asset.parents])
|
f.set_default("parents", asset.parents)
|
||||||
|
|
||||||
# notes
|
# notes
|
||||||
f.set_widget("notes", "notes")
|
f.set_widget("notes", "notes")
|
||||||
|
|
@ -293,11 +313,29 @@ class AssetMasterView(WuttaFarmMasterView):
|
||||||
f.set_default("image", asset.image_url)
|
f.set_default("image", asset.image_url)
|
||||||
|
|
||||||
def objectify(self, form):
|
def objectify(self, form):
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
asset = super().objectify(form)
|
asset = super().objectify(form)
|
||||||
|
data = form.validated
|
||||||
|
|
||||||
if self.creating:
|
if self.creating:
|
||||||
asset.asset_type = self.get_asset_type()
|
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
|
return asset
|
||||||
|
|
||||||
def get_asset_type(self):
|
def get_asset_type(self):
|
||||||
|
|
@ -322,6 +360,38 @@ class AssetMasterView(WuttaFarmMasterView):
|
||||||
|
|
||||||
return buttons
|
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):
|
def get_version_joins(self):
|
||||||
"""
|
"""
|
||||||
We override this to declare the relationship between the
|
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_filter("log_type", model.Log.log_type)
|
||||||
g.set_enum("log_type", get_log_type_enum(self.config, session=session))
|
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):
|
def get_row_action_url_view(self, log, i):
|
||||||
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ class CommonView(base.CommonView):
|
||||||
"farmos_quantities_standard.view",
|
"farmos_quantities_standard.view",
|
||||||
"farmos_quantity_types.list",
|
"farmos_quantity_types.list",
|
||||||
"farmos_quantity_types.view",
|
"farmos_quantity_types.view",
|
||||||
|
"farmos_seasons.list",
|
||||||
|
"farmos_seasons.view",
|
||||||
"farmos_structure_assets.list",
|
"farmos_structure_assets.list",
|
||||||
"farmos_structure_assets.view",
|
"farmos_structure_assets.view",
|
||||||
"farmos_structure_types.list",
|
"farmos_structure_types.list",
|
||||||
|
|
@ -132,6 +134,12 @@ class CommonView(base.CommonView):
|
||||||
"logs_observation.view",
|
"logs_observation.view",
|
||||||
"logs_observation.versions",
|
"logs_observation.versions",
|
||||||
"quick.eggs",
|
"quick.eggs",
|
||||||
|
"plant_types.list",
|
||||||
|
"plant_types.view",
|
||||||
|
"plant_types.versions",
|
||||||
|
"seasons.list",
|
||||||
|
"seasons.view",
|
||||||
|
"seasons.versions",
|
||||||
"structure_types.list",
|
"structure_types.list",
|
||||||
"structure_types.view",
|
"structure_types.view",
|
||||||
"structure_types.versions",
|
"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):
|
def includeme(config):
|
||||||
config.include("wuttafarm.web.views.farmos.users")
|
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.quantities")
|
||||||
config.include("wuttafarm.web.views.farmos.asset_types")
|
config.include("wuttafarm.web.views.farmos.asset_types")
|
||||||
config.include("wuttafarm.web.views.farmos.units")
|
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.land_assets")
|
||||||
config.include("wuttafarm.web.views.farmos.structure_types")
|
config.include("wuttafarm.web.views.farmos.structure_types")
|
||||||
config.include("wuttafarm.web.views.farmos.structures")
|
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.animal_types")
|
||||||
config.include("wuttafarm.web.views.farmos.animals")
|
config.include("wuttafarm.web.views.farmos.animals")
|
||||||
config.include("wuttafarm.web.views.farmos.groups")
|
config.include("wuttafarm.web.views.farmos.groups")
|
||||||
config.include("wuttafarm.web.views.farmos.plants")
|
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.log_types")
|
||||||
config.include("wuttafarm.web.views.farmos.logs_activity")
|
config.include("wuttafarm.web.views.farmos.logs_activity")
|
||||||
config.include("wuttafarm.web.views.farmos.logs_harvest")
|
config.include("wuttafarm.web.views.farmos.logs_harvest")
|
||||||
config.include("wuttafarm.web.views.farmos.logs_medical")
|
config.include("wuttafarm.web.views.farmos.logs_medical")
|
||||||
config.include("wuttafarm.web.views.farmos.logs_observation")
|
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,
|
NullableBooleanFilter,
|
||||||
DateTimeFilter,
|
DateTimeFilter,
|
||||||
)
|
)
|
||||||
from wuttafarm.web.forms.schema import FarmOSRef, FarmOSAssetRefs
|
from wuttafarm.web.forms.schema import FarmOSRef
|
||||||
|
|
||||||
|
|
||||||
class AnimalView(AssetMasterView):
|
class AnimalView(AssetMasterView):
|
||||||
|
|
@ -99,8 +99,7 @@ class AnimalView(AssetMasterView):
|
||||||
|
|
||||||
def get_farmos_api_includes(self):
|
def get_farmos_api_includes(self):
|
||||||
includes = super().get_farmos_api_includes()
|
includes = super().get_farmos_api_includes()
|
||||||
includes.add("animal_type")
|
includes.update(["animal_type"])
|
||||||
includes.add("group")
|
|
||||||
return includes
|
return includes
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
@ -131,10 +130,6 @@ class AnimalView(AssetMasterView):
|
||||||
g.set_sorter("sex", SimpleSorter("sex"))
|
g.set_sorter("sex", SimpleSorter("sex"))
|
||||||
g.set_filter("sex", StringFilter)
|
g.set_filter("sex", StringFilter)
|
||||||
|
|
||||||
# groups
|
|
||||||
g.set_label("groups", "Group Membership")
|
|
||||||
g.set_renderer("groups", self.render_groups_for_grid)
|
|
||||||
|
|
||||||
# is_sterile
|
# is_sterile
|
||||||
g.set_renderer("is_sterile", "boolean")
|
g.set_renderer("is_sterile", "boolean")
|
||||||
g.set_sorter("is_sterile", SimpleSorter("is_sterile"))
|
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)
|
url = self.request.route_url("farmos_animal_types.view", uuid=uuid)
|
||||||
return tags.link_to(value, url)
|
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):
|
def get_instance(self):
|
||||||
|
|
||||||
data = super().get_instance()
|
data = super().get_instance()
|
||||||
|
|
@ -192,8 +175,6 @@ class AnimalView(AssetMasterView):
|
||||||
sterile = animal["attributes"]["is_castrated"]
|
sterile = animal["attributes"]["is_castrated"]
|
||||||
|
|
||||||
animal_type_object = None
|
animal_type_object = None
|
||||||
group_objects = []
|
|
||||||
group_names = []
|
|
||||||
if relationships := animal.get("relationships"):
|
if relationships := animal.get("relationships"):
|
||||||
|
|
||||||
if animal_type := relationships.get("animal_type"):
|
if animal_type := relationships.get("animal_type"):
|
||||||
|
|
@ -203,24 +184,11 @@ class AnimalView(AssetMasterView):
|
||||||
"name": animal_type["attributes"]["name"],
|
"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(
|
normal.update(
|
||||||
{
|
{
|
||||||
"animal_type": animal_type_object,
|
"animal_type": animal_type_object,
|
||||||
"animal_type_uuid": animal_type_object["uuid"],
|
"animal_type_uuid": animal_type_object["uuid"],
|
||||||
"animal_type_name": animal_type_object["name"],
|
"animal_type_name": animal_type_object["name"],
|
||||||
"groups": group_objects,
|
|
||||||
"group_names": group_names,
|
|
||||||
"birthdate": birthdate,
|
"birthdate": birthdate,
|
||||||
"sex": animal["attributes"]["sex"] or colander.null,
|
"sex": animal["attributes"]["sex"] or colander.null,
|
||||||
"is_sterile": sterile,
|
"is_sterile": sterile,
|
||||||
|
|
@ -271,12 +239,6 @@ class AnimalView(AssetMasterView):
|
||||||
# is_sterile
|
# is_sterile
|
||||||
f.set_node("is_sterile", colander.Boolean())
|
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):
|
def get_api_payload(self, animal):
|
||||||
payload = super().get_api_payload(animal)
|
payload = super().get_api_payload(animal)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ Base class for Asset master views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
import requests
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
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.forms.widgets import ImageWidget
|
||||||
from wuttafarm.web.grids import (
|
from wuttafarm.web.grids import (
|
||||||
ResourceData,
|
ResourceData,
|
||||||
|
|
@ -75,6 +76,23 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
"archived": {"active": True, "verb": "is_false"},
|
"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):
|
def get_grid_data(self, **kwargs):
|
||||||
return ResourceData(
|
return ResourceData(
|
||||||
self.config,
|
self.config,
|
||||||
|
|
@ -110,6 +128,10 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
# locations
|
# locations
|
||||||
g.set_renderer("locations", self.render_locations_for_grid)
|
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
|
# archived
|
||||||
g.set_renderer("archived", "boolean")
|
g.set_renderer("archived", "boolean")
|
||||||
g.set_sorter("archived", SimpleSorter("archived"))
|
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 tags.image(url, f"thumbnail for {self.get_model_title()}")
|
||||||
return None
|
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):
|
def render_locations_for_grid(self, asset, field, value):
|
||||||
locations = []
|
locations = []
|
||||||
for location in value:
|
for location in value:
|
||||||
|
|
@ -139,14 +175,19 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_farmos_api_includes(self):
|
def get_farmos_api_includes(self):
|
||||||
return {"asset_type", "location", "owner", "image"}
|
return {"asset_type", "location", "group", "owner", "image"}
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
result = self.farmos_client.asset.get_id(
|
try:
|
||||||
self.farmos_asset_type,
|
result = self.farmos_client.asset.get_id(
|
||||||
self.request.matchdict["uuid"],
|
self.farmos_asset_type,
|
||||||
params={"include": ",".join(self.get_farmos_api_includes())},
|
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
|
self.raw_json = result
|
||||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
included = {obj["id"]: obj for obj in result.get("included", [])}
|
||||||
return self.normalize_asset(result["data"], included)
|
return self.normalize_asset(result["data"], included)
|
||||||
|
|
@ -170,6 +211,7 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
owner_names = []
|
owner_names = []
|
||||||
location_objects = []
|
location_objects = []
|
||||||
location_names = []
|
location_names = []
|
||||||
|
group_objects = []
|
||||||
thumbnail_url = None
|
thumbnail_url = None
|
||||||
image_url = None
|
image_url = None
|
||||||
if relationships := asset.get("relationships"):
|
if relationships := asset.get("relationships"):
|
||||||
|
|
@ -203,6 +245,16 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
location_objects.append(location)
|
location_objects.append(location)
|
||||||
location_names.append(location["name"])
|
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"):
|
if images := relationships.get("image"):
|
||||||
for image in images["data"]:
|
for image in images["data"]:
|
||||||
if image := included.get(image["id"]):
|
if image := included.get(image["id"]):
|
||||||
|
|
@ -217,11 +269,14 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
"name": asset["attributes"]["name"],
|
"name": asset["attributes"]["name"],
|
||||||
"asset_type": asset_type_object,
|
"asset_type": asset_type_object,
|
||||||
"asset_type_name": asset_type_name,
|
"asset_type_name": asset_type_name,
|
||||||
|
"is_location": asset["attributes"]["is_location"],
|
||||||
|
"is_fixed": asset["attributes"]["is_fixed"],
|
||||||
"notes": notes or colander.null,
|
"notes": notes or colander.null,
|
||||||
"owners": owner_objects,
|
"owners": owner_objects,
|
||||||
"owner_names": owner_names,
|
"owner_names": owner_names,
|
||||||
"locations": location_objects,
|
"locations": location_objects,
|
||||||
"location_names": location_names,
|
"location_names": location_names,
|
||||||
|
"groups": group_objects,
|
||||||
"archived": archived,
|
"archived": archived,
|
||||||
"thumbnail_url": thumbnail_url or colander.null,
|
"thumbnail_url": thumbnail_url or colander.null,
|
||||||
"image_url": image_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_label("locations", "Current Location")
|
||||||
f.set_node("locations", FarmOSLocationRefs(self.request))
|
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
|
# owners
|
||||||
if self.creating or self.editing:
|
if self.creating or self.editing:
|
||||||
f.remove("owners") # TODO
|
f.remove("owners") # TODO
|
||||||
|
|
@ -253,6 +314,16 @@ class AssetMasterView(FarmOSMasterView):
|
||||||
f.set_widget("notes", "notes")
|
f.set_widget("notes", "notes")
|
||||||
f.set_required("notes", False)
|
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
|
# archived
|
||||||
f.set_node("archived", colander.Boolean())
|
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,
|
LogQuick,
|
||||||
Notes,
|
Notes,
|
||||||
)
|
)
|
||||||
from wuttafarm.web.util import render_quantity_objects
|
from wuttafarm.web.util import render_quantity_objects, render_quantity_object
|
||||||
|
|
||||||
|
|
||||||
class LogMasterView(FarmOSMasterView):
|
class LogMasterView(FarmOSMasterView):
|
||||||
|
|
@ -199,7 +199,20 @@ class LogMasterView(FarmOSMasterView):
|
||||||
)
|
)
|
||||||
self.raw_json = result
|
self.raw_json = result
|
||||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
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):
|
def get_instance_title(self, log):
|
||||||
return log["name"]
|
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.master import TaxonomyMasterView
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
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
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -75,6 +80,43 @@ class PlantTypeView(TaxonomyMasterView):
|
||||||
return buttons
|
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):
|
class PlantAssetView(FarmOSMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for farmOS Plant Assets
|
Master view for farmOS Plant Assets
|
||||||
|
|
@ -89,6 +131,10 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
|
|
||||||
farmos_refurl_path = "/assets/plant"
|
farmos_refurl_path = "/assets/plant"
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"seasons": "Season",
|
||||||
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"name",
|
"name",
|
||||||
"archived",
|
"archived",
|
||||||
|
|
@ -99,6 +145,7 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"plant_types",
|
"plant_types",
|
||||||
|
"seasons",
|
||||||
"archived",
|
"archived",
|
||||||
"owners",
|
"owners",
|
||||||
"location",
|
"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
|
# add location
|
||||||
if location := relationships.get("location"):
|
if location := relationships.get("location"):
|
||||||
if location["data"]:
|
if location["data"]:
|
||||||
|
|
@ -199,22 +261,14 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
return plant["name"]
|
return plant["name"]
|
||||||
|
|
||||||
def normalize_plant(self, plant):
|
def normalize_plant(self, plant):
|
||||||
|
normal = self.normal.normalize_farmos_asset(plant)
|
||||||
if notes := plant["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
if self.farmos_4x:
|
|
||||||
archived = plant["attributes"]["archived"]
|
|
||||||
else:
|
|
||||||
archived = plant["attributes"]["status"] == "archived"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uuid": plant["id"],
|
"uuid": normal["uuid"],
|
||||||
"drupal_id": plant["attributes"]["drupal_internal__id"],
|
"drupal_id": normal["drupal_id"],
|
||||||
"name": plant["attributes"]["name"],
|
"name": normal["asset_name"],
|
||||||
"location": colander.null, # TODO
|
"location": colander.null, # TODO
|
||||||
"archived": archived,
|
"archived": normal["archived"],
|
||||||
"notes": notes or colander.null,
|
"notes": normal["notes"] or colander.null,
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
|
|
@ -225,6 +279,9 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
# plant_types
|
# plant_types
|
||||||
f.set_node("plant_types", FarmOSPlantTypes(self.request))
|
f.set_node("plant_types", FarmOSPlantTypes(self.request))
|
||||||
|
|
||||||
|
# seasons
|
||||||
|
f.set_node("seasons", FarmOSRefs(self.request, "farmos_seasons"))
|
||||||
|
|
||||||
# location
|
# location
|
||||||
f.set_node("location", StructureType(self.request))
|
f.set_node("location", StructureType(self.request))
|
||||||
|
|
||||||
|
|
@ -279,6 +336,9 @@ def defaults(config, **kwargs):
|
||||||
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
||||||
PlantTypeView.defaults(config)
|
PlantTypeView.defaults(config)
|
||||||
|
|
||||||
|
SeasonView = kwargs.get("SeasonView", base["SeasonView"])
|
||||||
|
SeasonView.defaults(config)
|
||||||
|
|
||||||
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
||||||
PlantAssetView.defaults(config)
|
PlantAssetView.defaults(config)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,13 @@ View for farmOS Quantity Types
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
import requests
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDateTime
|
from wuttaweb.forms.schema import WuttaDateTime
|
||||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
from wuttafarm.web.forms.schema import FarmOSUnitRef, FarmOSRefs
|
||||||
from wuttafarm.web.grids import ResourceData
|
from wuttafarm.web.grids import ResourceData
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -142,6 +143,7 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
sort_defaults = ("drupal_id", "desc")
|
sort_defaults = ("drupal_id", "desc")
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
|
"quantity_type_name",
|
||||||
"measure",
|
"measure",
|
||||||
"value",
|
"value",
|
||||||
"units",
|
"units",
|
||||||
|
|
@ -171,6 +173,7 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
|
|
||||||
# as_text
|
# as_text
|
||||||
g.set_renderer("as_text", self.render_as_text_for_grid)
|
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||||
|
g.set_link("as_text")
|
||||||
|
|
||||||
# measure
|
# measure
|
||||||
g.set_renderer("measure", self.render_measure_for_grid)
|
g.set_renderer("measure", self.render_measure_for_grid)
|
||||||
|
|
@ -203,14 +206,26 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
return qty["value"]["decimal"]
|
return qty["value"]["decimal"]
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
quantity = self.farmos_client.resource.get_id(
|
# TODO: this pattern should be repeated for other views
|
||||||
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
|
try:
|
||||||
)
|
result = self.farmos_client.resource.get_id(
|
||||||
self.raw_json = quantity
|
"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
|
# add units
|
||||||
if units := relationships.get("units"):
|
if units := relationships.get("units"):
|
||||||
|
|
@ -278,6 +293,11 @@ class QuantityMasterView(FarmOSMasterView):
|
||||||
f = form
|
f = form
|
||||||
super().configure_form(f)
|
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
|
# created
|
||||||
f.set_node("created", WuttaDateTime(self.request))
|
f.set_node("created", WuttaDateTime(self.request))
|
||||||
f.set_widget("created", WuttaDateTimeWidget(self.request))
|
f.set_widget("created", WuttaDateTimeWidget(self.request))
|
||||||
|
|
@ -303,6 +323,7 @@ class StandardQuantityView(QuantityMasterView):
|
||||||
url_prefix = "/farmOS/quantities/standard"
|
url_prefix = "/farmOS/quantities/standard"
|
||||||
|
|
||||||
farmos_quantity_type = "standard"
|
farmos_quantity_type = "standard"
|
||||||
|
farmos_refurl_path = "/log-quantities/standard"
|
||||||
|
|
||||||
def get_xref_buttons(self, standard_quantity):
|
def get_xref_buttons(self, standard_quantity):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
@ -329,6 +350,90 @@ class StandardQuantityView(QuantityMasterView):
|
||||||
return buttons
|
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):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
|
|
@ -340,6 +445,11 @@ def defaults(config, **kwargs):
|
||||||
)
|
)
|
||||||
StandardQuantityView.defaults(config)
|
StandardQuantityView.defaults(config)
|
||||||
|
|
||||||
|
MaterialQuantityView = kwargs.get(
|
||||||
|
"MaterialQuantityView", base["MaterialQuantityView"]
|
||||||
|
)
|
||||||
|
MaterialQuantityView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(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",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
||||||
form_fields = [
|
def configure_form(self, f):
|
||||||
"asset_name",
|
super().configure_form(f)
|
||||||
"notes",
|
|
||||||
"asset_type",
|
# produces_eggs
|
||||||
"produces_eggs",
|
f.fields.insert_after("asset_type", "produces_eggs")
|
||||||
"archived",
|
|
||||||
"drupal_id",
|
|
||||||
"farmos_uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import LogType, Log
|
from wuttafarm.db.model import LogType, Log
|
||||||
from wuttafarm.web.forms.schema import AssetRefs, LogQuantityRefs, OwnerRefs
|
from wuttafarm.web.forms.schema import AssetRefs, QuantityRefs, OwnerRefs
|
||||||
from wuttafarm.util import get_log_type_enum
|
from wuttafarm.util import get_log_type_enum
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -256,26 +256,21 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
f.set_default("timestamp", self.app.make_utc())
|
f.set_default("timestamp", self.app.make_utc())
|
||||||
|
|
||||||
# assets
|
# assets
|
||||||
if self.creating or self.editing:
|
f.set_node("assets", AssetRefs(self.request))
|
||||||
f.remove("assets") # TODO: need to support this
|
f.set_required("assets", False)
|
||||||
else:
|
if not self.creating:
|
||||||
f.set_node("assets", AssetRefs(self.request))
|
|
||||||
# nb. must explicity declare value for non-standard field
|
# nb. must explicity declare value for non-standard field
|
||||||
f.set_default("assets", log.assets)
|
f.set_default("assets", log.assets)
|
||||||
|
|
||||||
# groups
|
# groups
|
||||||
if self.creating or self.editing:
|
f.set_node("groups", AssetRefs(self.request, is_group=True))
|
||||||
f.remove("groups") # TODO: need to support this
|
if not self.creating:
|
||||||
else:
|
|
||||||
f.set_node("groups", AssetRefs(self.request))
|
|
||||||
# nb. must explicity declare value for non-standard field
|
# nb. must explicity declare value for non-standard field
|
||||||
f.set_default("groups", log.groups)
|
f.set_default("groups", log.groups)
|
||||||
|
|
||||||
# locations
|
# locations
|
||||||
if self.creating or self.editing:
|
f.set_node("locations", AssetRefs(self.request, is_location=True))
|
||||||
f.remove("locations") # TODO: need to support this
|
if not self.creating:
|
||||||
else:
|
|
||||||
f.set_node("locations", AssetRefs(self.request))
|
|
||||||
# nb. must explicity declare value for non-standard field
|
# nb. must explicity declare value for non-standard field
|
||||||
f.set_default("locations", log.locations)
|
f.set_default("locations", log.locations)
|
||||||
|
|
||||||
|
|
@ -292,12 +287,12 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
f.set_readonly("log_type")
|
f.set_readonly("log_type")
|
||||||
|
|
||||||
# quantities
|
# quantities
|
||||||
if self.creating or self.editing:
|
f.set_node("quantities", QuantityRefs(self.request))
|
||||||
f.remove("quantities") # TODO: need to support this
|
if not self.creating:
|
||||||
else:
|
|
||||||
f.set_node("quantities", LogQuantityRefs(self.request))
|
|
||||||
# nb. must explicity declare value for non-standard field
|
# 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
|
# notes
|
||||||
f.set_widget("notes", "notes")
|
f.set_widget("notes", "notes")
|
||||||
|
|
@ -324,13 +319,141 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
|
|
||||||
def objectify(self, form):
|
def objectify(self, form):
|
||||||
log = super().objectify(form)
|
log = super().objectify(form)
|
||||||
|
data = form.validated
|
||||||
|
|
||||||
if self.creating:
|
if self.creating:
|
||||||
model_class = self.get_model_class()
|
|
||||||
|
# log_type
|
||||||
log.log_type = self.get_farmos_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
|
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):
|
def get_farmos_url(self, log):
|
||||||
return self.app.get_farmos_url(f"/log/{log.drupal_id}")
|
return self.app.get_farmos_url(f"/log/{log.drupal_id}")
|
||||||
|
|
||||||
|
|
@ -368,6 +491,7 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
return super().get_version_joins() + [
|
return super().get_version_joins() + [
|
||||||
model.Log,
|
model.Log,
|
||||||
(model.LogAsset, "log_uuid", "uuid"),
|
(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
|
Base class for WuttaFarm master views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
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
|
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")
|
f.set_readonly("drupal_id")
|
||||||
|
|
||||||
def persist(self, obj, session=None):
|
def persist(self, obj, session=None):
|
||||||
|
session = session or self.Session()
|
||||||
|
|
||||||
# save per usual
|
# save per usual
|
||||||
super().persist(obj, session)
|
super().persist(obj, session)
|
||||||
|
|
||||||
# maybe also sync change to farmOS
|
# maybe also sync change to farmOS
|
||||||
if self.app.is_farmos_mirror():
|
if self.app.is_farmos_mirror():
|
||||||
|
if self.creating:
|
||||||
|
session.flush() # need the new uuid
|
||||||
client = get_farmos_client_for_user(self.request)
|
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)
|
self.app.auto_sync_to_farmos(obj, client=client, require=False)
|
||||||
|
|
||||||
def get_farmos_entity_type(self):
|
def get_farmos_entity_type(self):
|
||||||
|
|
@ -141,7 +168,130 @@ class WuttaFarmMasterView(MasterView):
|
||||||
|
|
||||||
# maybe delete from farmOS also
|
# maybe delete from farmOS also
|
||||||
if farmos_uuid:
|
if farmos_uuid:
|
||||||
entity_type = self.get_farmos_entity_type()
|
|
||||||
bundle = self.get_farmos_bundle()
|
|
||||||
client = get_farmos_client_for_user(self.request)
|
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)
|
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.forms.schema import WuttaDictEnum
|
||||||
from wuttaweb.util import get_form_data
|
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.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.forms.widgets import ImageWidget
|
||||||
from wuttafarm.web.util import get_farmos_client_for_user
|
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):
|
class PlantAssetView(AssetMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Plant Assets
|
Master view for Plant Assets
|
||||||
|
|
@ -209,6 +369,7 @@ class PlantAssetView(AssetMasterView):
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"plant_types": "Crop/Variety",
|
"plant_types": "Crop/Variety",
|
||||||
|
"seasons": "Season",
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
|
@ -220,21 +381,6 @@ class PlantAssetView(AssetMasterView):
|
||||||
"archived",
|
"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):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
@ -262,23 +408,33 @@ class PlantAssetView(AssetMasterView):
|
||||||
plant = f.model_instance
|
plant = f.model_instance
|
||||||
|
|
||||||
# plant_types
|
# plant_types
|
||||||
|
f.fields.insert_after("asset_name", "plant_types")
|
||||||
f.set_node("plant_types", PlantTypeRefs(self.request))
|
f.set_node("plant_types", PlantTypeRefs(self.request))
|
||||||
if not self.creating:
|
if not self.creating:
|
||||||
# nb. must explcitly declare value for non-standard field
|
# nb. must explcitly declare value for non-standard field
|
||||||
f.set_default("plant_types", [pt.uuid for pt in plant.plant_types])
|
f.set_default("plant_types", [pt.uuid for pt in plant.plant_types])
|
||||||
|
|
||||||
# season
|
# season
|
||||||
if self.creating or self.editing:
|
f.fields.insert_after("plant_types", "seasons")
|
||||||
f.remove("season") # TODO: add support for this
|
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):
|
def objectify(self, form):
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
plant = super().objectify(form)
|
plant = super().objectify(form)
|
||||||
data = form.validated
|
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]
|
current = [pt.uuid for pt in plant.plant_types]
|
||||||
desired = data["plant_types"]
|
|
||||||
|
|
||||||
for uuid in desired:
|
for uuid in desired:
|
||||||
if uuid not in current:
|
if uuid not in current:
|
||||||
|
|
@ -292,7 +448,22 @@ class PlantAssetView(AssetMasterView):
|
||||||
assert plant_type
|
assert plant_type
|
||||||
plant.plant_types.remove(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):
|
def defaults(config, **kwargs):
|
||||||
|
|
@ -301,6 +472,9 @@ def defaults(config, **kwargs):
|
||||||
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
||||||
PlantTypeView.defaults(config)
|
PlantTypeView.defaults(config)
|
||||||
|
|
||||||
|
SeasonView = kwargs.get("SeasonView", base["SeasonView"])
|
||||||
|
SeasonView.defaults(config)
|
||||||
|
|
||||||
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
||||||
PlantAssetView.defaults(config)
|
PlantAssetView.defaults(config)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,19 @@ Master view for Quantities
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
|
from wuttafarm.db.model import (
|
||||||
from wuttafarm.web.forms.schema import UnitRef, LogRef
|
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):
|
def get_quantity_type_enum(config):
|
||||||
|
|
@ -100,17 +108,28 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
Base class for Quantity master views
|
Base class for Quantity master views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
farmos_entity_type = "quantity"
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"log_id": "Log ID",
|
||||||
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
"as_text",
|
"log_id",
|
||||||
"quantity_type",
|
"log_status",
|
||||||
|
"log_timestamp",
|
||||||
|
"log_type",
|
||||||
|
"log_name",
|
||||||
|
"log_assets",
|
||||||
"measure",
|
"measure",
|
||||||
"value",
|
"value",
|
||||||
"units",
|
"units",
|
||||||
"label",
|
"label",
|
||||||
|
"quantity_type",
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = ("drupal_id", "desc")
|
sort_defaults = ("log_timestamp", "desc")
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"quantity_type",
|
"quantity_type",
|
||||||
|
|
@ -129,10 +148,15 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
model_class = self.get_model_class()
|
model_class = self.get_model_class()
|
||||||
session = session or self.Session()
|
session = session or self.Session()
|
||||||
|
|
||||||
query = session.query(model_class)
|
query = session.query(model_class)
|
||||||
if model_class is not model.Quantity:
|
if model_class is not model.Quantity:
|
||||||
query = query.join(model.Quantity)
|
query = query.join(model.Quantity)
|
||||||
|
|
||||||
query = query.join(model.Measure).join(model.Unit)
|
query = query.join(model.Measure).join(model.Unit)
|
||||||
|
|
||||||
|
query = query.outerjoin(model.LogQuantity).outerjoin(model.Log)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
@ -140,14 +164,39 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
model_class = self.get_model_class()
|
model_class = self.get_model_class()
|
||||||
|
session = self.Session()
|
||||||
|
|
||||||
# drupal_id
|
# drupal_id
|
||||||
g.set_label("drupal_id", "ID", column_only=True)
|
g.set_label("drupal_id", "ID", column_only=True)
|
||||||
g.set_sorter("drupal_id", model.Quantity.drupal_id)
|
g.set_sorter("drupal_id", model.Quantity.drupal_id)
|
||||||
|
|
||||||
# as_text
|
# log_id
|
||||||
g.set_renderer("as_text", self.render_as_text_for_grid)
|
g.set_renderer("log_id", self.render_log_id_for_grid)
|
||||||
g.set_link("as_text")
|
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
|
# quantity_type
|
||||||
if model_class is not model.Quantity:
|
if model_class is not model.Quantity:
|
||||||
|
|
@ -177,8 +226,47 @@ class QuantityMasterView(WuttaFarmMasterView):
|
||||||
|
|
||||||
g.add_action("view", icon="eye", url=quantity_url)
|
g.add_action("view", icon="eye", url=quantity_url)
|
||||||
|
|
||||||
def render_as_text_for_grid(self, quantity, field, value):
|
def render_log_id_for_grid(self, quantity, field, value):
|
||||||
return quantity.render_as_text(self.config)
|
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):
|
def render_value_for_grid(self, quantity, field, value):
|
||||||
value = quantity.value_numerator / quantity.value_denominator
|
value = quantity.value_numerator / quantity.value_denominator
|
||||||
|
|
@ -271,6 +359,8 @@ class AllQuantityView(QuantityMasterView):
|
||||||
deletable = False
|
deletable = False
|
||||||
model_is_versioned = False
|
model_is_versioned = False
|
||||||
|
|
||||||
|
farmos_refurl_path = "/log-quantities"
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantityView(QuantityMasterView):
|
class StandardQuantityView(QuantityMasterView):
|
||||||
"""
|
"""
|
||||||
|
|
@ -281,6 +371,77 @@ class StandardQuantityView(QuantityMasterView):
|
||||||
route_prefix = "quantities_standard"
|
route_prefix = "quantities_standard"
|
||||||
url_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):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
@ -296,6 +457,11 @@ def defaults(config, **kwargs):
|
||||||
)
|
)
|
||||||
StandardQuantityView.defaults(config)
|
StandardQuantityView.defaults(config)
|
||||||
|
|
||||||
|
MaterialQuantityView = kwargs.get(
|
||||||
|
"MaterialQuantityView", base["MaterialQuantityView"]
|
||||||
|
)
|
||||||
|
MaterialQuantityView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ Quick Form for "Eggs"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform.widget import SelectWidget
|
from deform.widget import SelectWidget
|
||||||
|
|
@ -331,13 +333,43 @@ class EggsQuickForm(QuickFormView):
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
if self.app.is_farmos_mirror():
|
if self.app.is_farmos_mirror():
|
||||||
if new_unit:
|
thread = threading.Thread(
|
||||||
self.app.auto_sync_to_farmos(unit, client=self.farmos_client)
|
target=self.auto_sync_to_farmos,
|
||||||
self.app.auto_sync_to_farmos(quantity, client=self.farmos_client)
|
args=(log.uuid, quantity.uuid, new_unit.uuid if new_unit else None),
|
||||||
self.app.auto_sync_to_farmos(log, client=self.farmos_client)
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
return log
|
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):
|
def redirect_after_save(self, log):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,19 @@ class MeasureView(WuttaFarmMasterView):
|
||||||
url_prefix = "/measures"
|
url_prefix = "/measures"
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
"ordinal",
|
||||||
"name",
|
"name",
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = "name"
|
sort_defaults = "ordinal"
|
||||||
|
|
||||||
filter_defaults = {
|
filter_defaults = {
|
||||||
"name": {"active": True, "verb": "contains"},
|
"name": {"active": True, "verb": "contains"},
|
||||||
}
|
}
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
|
"ordinal",
|
||||||
"name",
|
"name",
|
||||||
"drupal_id",
|
"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