Compare commits
No commits in common. "e7ef5c3d32255edeadd7789e27a7340ee355de4f" and "5ee2db267a678a4374ebb511b79ded408b0c25d1" have entirely different histories.
e7ef5c3d32
...
5ee2db267a
45 changed files with 530 additions and 4783 deletions
|
|
@ -51,44 +51,6 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
self.handlers["farmos"] = factory(self.config)
|
self.handlers["farmos"] = factory(self.config)
|
||||||
return self.handlers["farmos"]
|
return self.handlers["farmos"]
|
||||||
|
|
||||||
def get_farmos_integration_mode(self):
|
|
||||||
"""
|
|
||||||
Returns the integration mode for farmOS, i.e. to control the
|
|
||||||
app's behavior regarding that.
|
|
||||||
"""
|
|
||||||
enum = self.enum
|
|
||||||
return self.config.get(
|
|
||||||
f"{self.appname}.farmos_integration_mode",
|
|
||||||
default=enum.FARMOS_INTEGRATION_MODE_WRAPPER,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_farmos_mirror(self):
|
|
||||||
"""
|
|
||||||
Returns ``True`` if the app is configured in "mirror"
|
|
||||||
integration mode with regard to farmOS.
|
|
||||||
"""
|
|
||||||
enum = self.enum
|
|
||||||
mode = self.get_farmos_integration_mode()
|
|
||||||
return mode == enum.FARMOS_INTEGRATION_MODE_MIRROR
|
|
||||||
|
|
||||||
def is_farmos_wrapper(self):
|
|
||||||
"""
|
|
||||||
Returns ``True`` if the app is configured in "wrapper"
|
|
||||||
integration mode with regard to farmOS.
|
|
||||||
"""
|
|
||||||
enum = self.enum
|
|
||||||
mode = self.get_farmos_integration_mode()
|
|
||||||
return mode == enum.FARMOS_INTEGRATION_MODE_WRAPPER
|
|
||||||
|
|
||||||
def is_standalone(self):
|
|
||||||
"""
|
|
||||||
Returns ``True`` if the app is configured in "standalone" mode
|
|
||||||
with regard to farmOS.
|
|
||||||
"""
|
|
||||||
enum = self.enum
|
|
||||||
mode = self.get_farmos_integration_mode()
|
|
||||||
return mode == enum.FARMOS_INTEGRATION_MODE_NONE
|
|
||||||
|
|
||||||
def get_farmos_url(self, *args, **kwargs):
|
def get_farmos_url(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get a farmOS URL. This is a convenience wrapper around
|
Get a farmOS URL. This is a convenience wrapper around
|
||||||
|
|
@ -123,20 +85,7 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
handler = self.get_farmos_handler()
|
handler = self.get_farmos_handler()
|
||||||
return handler.is_farmos_4x(*args, **kwargs)
|
return handler.is_farmos_4x(*args, **kwargs)
|
||||||
|
|
||||||
def get_normalizer(self, farmos_client=None):
|
def export_to_farmos(self, obj, require=True):
|
||||||
"""
|
|
||||||
Get the configured farmOS integration handler.
|
|
||||||
|
|
||||||
:rtype: :class:`~wuttafarm.farmos.FarmOSHandler`
|
|
||||||
"""
|
|
||||||
spec = self.config.get(
|
|
||||||
f"{self.appname}.normalizer_spec",
|
|
||||||
default="wuttafarm.normal:Normalizer",
|
|
||||||
)
|
|
||||||
factory = self.load_object(spec)
|
|
||||||
return factory(self.config, farmos_client)
|
|
||||||
|
|
||||||
def auto_sync_to_farmos(self, obj, model_name=None, require=True):
|
|
||||||
"""
|
"""
|
||||||
Export the given object to farmOS, using configured handler.
|
Export the given object to farmOS, using configured handler.
|
||||||
|
|
||||||
|
|
@ -154,7 +103,6 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
"""
|
"""
|
||||||
handler = self.app.get_import_handler("export.to_farmos.from_wuttafarm")
|
handler = self.app.get_import_handler("export.to_farmos.from_wuttafarm")
|
||||||
|
|
||||||
if not model_name:
|
|
||||||
model_name = type(obj).__name__
|
model_name = type(obj).__name__
|
||||||
if model_name not in handler.importers:
|
if model_name not in handler.importers:
|
||||||
if require:
|
if require:
|
||||||
|
|
@ -169,37 +117,6 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
normal = importer.normalize_source_object(obj)
|
normal = importer.normalize_source_object(obj)
|
||||||
importer.process_data(source_data=[normal])
|
importer.process_data(source_data=[normal])
|
||||||
|
|
||||||
def auto_sync_from_farmos(self, obj, model_name, require=True):
|
|
||||||
"""
|
|
||||||
Import the given object from farmOS, using configured handler.
|
|
||||||
|
|
||||||
:param obj: Any data record from farmOS.
|
|
||||||
|
|
||||||
:param model_name': Model name for the importer to use,
|
|
||||||
e.g. ``"AnimalAsset"``.
|
|
||||||
|
|
||||||
:param require: If true, this will *require* the import
|
|
||||||
handler to support objects of the given type. If false,
|
|
||||||
then nothing will happen / import is silently skipped when
|
|
||||||
there is no such importer.
|
|
||||||
"""
|
|
||||||
handler = self.app.get_import_handler("import.to_wuttafarm.from_farmos")
|
|
||||||
|
|
||||||
if model_name not in handler.importers:
|
|
||||||
if require:
|
|
||||||
raise ValueError(f"no importer found for {model_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# nb. begin txn to establish the API client
|
|
||||||
# TODO: should probably use current user oauth2 token instead
|
|
||||||
# of always making a new one here, which is what happens IIUC
|
|
||||||
handler.begin_source_transaction()
|
|
||||||
with self.short_session(commit=True) as session:
|
|
||||||
handler.target_session = session
|
|
||||||
importer = handler.get_importer(model_name, caches_target=False)
|
|
||||||
normal = importer.normalize_source_object(obj)
|
|
||||||
importer.process_data(source_data=[normal])
|
|
||||||
|
|
||||||
|
|
||||||
class WuttaFarmAppProvider(base.AppProvider):
|
class WuttaFarmAppProvider(base.AppProvider):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class WuttaFarmConfig(WuttaConfigExtension):
|
||||||
|
|
||||||
# web app menu
|
# web app menu
|
||||||
config.setdefault(
|
config.setdefault(
|
||||||
f"{config.appname}.web.menus.handler.default_spec",
|
f"{config.appname}.web.menus.handler.spec",
|
||||||
"wuttafarm.web.menus:WuttaFarmMenuHandler",
|
"wuttafarm.web.menus:WuttaFarmMenuHandler",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
"""add Quantity Types
|
|
||||||
|
|
||||||
Revision ID: 1f98d27cabeb
|
|
||||||
Revises: ea88e72a5fa5
|
|
||||||
Create Date: 2026-02-18 21:03:52.245619
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import wuttjamaican.db.util
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "1f98d27cabeb"
|
|
||||||
down_revision: Union[str, None] = "ea88e72a5fa5"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
|
|
||||||
# quantity_type
|
|
||||||
op.create_table(
|
|
||||||
"quantity_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.String(length=50), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity_type")),
|
|
||||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_quantity_type_drupal_id")),
|
|
||||||
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_quantity_type_farmos_uuid")),
|
|
||||||
sa.UniqueConstraint("name", name=op.f("uq_quantity_type_name")),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"quantity_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.String(length=50), 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_type_version")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_type_version_end_transaction_id"),
|
|
||||||
"quantity_type_version",
|
|
||||||
["end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_type_version_operation_type"),
|
|
||||||
"quantity_type_version",
|
|
||||||
["operation_type"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_quantity_type_version_pk_transaction_id",
|
|
||||||
"quantity_type_version",
|
|
||||||
["uuid", sa.literal_column("transaction_id DESC")],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_quantity_type_version_pk_validity",
|
|
||||||
"quantity_type_version",
|
|
||||||
["uuid", "transaction_id", "end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_type_version_transaction_id"),
|
|
||||||
"quantity_type_version",
|
|
||||||
["transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
|
|
||||||
# quantity_type
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_type_version_transaction_id"),
|
|
||||||
table_name="quantity_type_version",
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
"ix_quantity_type_version_pk_validity", table_name="quantity_type_version"
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
"ix_quantity_type_version_pk_transaction_id", table_name="quantity_type_version"
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_type_version_operation_type"),
|
|
||||||
table_name="quantity_type_version",
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_type_version_end_transaction_id"),
|
|
||||||
table_name="quantity_type_version",
|
|
||||||
)
|
|
||||||
op.drop_table("quantity_type_version")
|
|
||||||
op.drop_table("quantity_type")
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
"""add Standard Quantities
|
|
||||||
|
|
||||||
Revision ID: 5b6c87d8cddf
|
|
||||||
Revises: 1f98d27cabeb
|
|
||||||
Create Date: 2026-02-19 15:42:19.691148
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import wuttjamaican.db.util
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "5b6c87d8cddf"
|
|
||||||
down_revision: Union[str, None] = "1f98d27cabeb"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
|
|
||||||
# measure
|
|
||||||
op.create_table(
|
|
||||||
"measure",
|
|
||||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
|
||||||
sa.Column("name", sa.String(length=100), nullable=False),
|
|
||||||
sa.Column("drupal_id", sa.String(length=20), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_measure")),
|
|
||||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_measure_drupal_id")),
|
|
||||||
sa.UniqueConstraint("name", name=op.f("uq_measure_name")),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"measure_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(
|
|
||||||
"drupal_id", sa.String(length=20), 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_measure_version")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_measure_version_end_transaction_id"),
|
|
||||||
"measure_version",
|
|
||||||
["end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_measure_version_operation_type"),
|
|
||||||
"measure_version",
|
|
||||||
["operation_type"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_measure_version_pk_transaction_id",
|
|
||||||
"measure_version",
|
|
||||||
["uuid", sa.literal_column("transaction_id DESC")],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_measure_version_pk_validity",
|
|
||||||
"measure_version",
|
|
||||||
["uuid", "transaction_id", "end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_measure_version_transaction_id"),
|
|
||||||
"measure_version",
|
|
||||||
["transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# quantity
|
|
||||||
op.create_table(
|
|
||||||
"quantity",
|
|
||||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
|
||||||
sa.Column("quantity_type_id", sa.String(length=50), nullable=False),
|
|
||||||
sa.Column("measure_id", sa.String(length=20), nullable=False),
|
|
||||||
sa.Column("value_numerator", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("value_denominator", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("units_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
|
||||||
sa.Column("label", 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.ForeignKeyConstraint(
|
|
||||||
["measure_id"],
|
|
||||||
["measure.drupal_id"],
|
|
||||||
name=op.f("fk_quantity_measure_id_measure"),
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["quantity_type_id"],
|
|
||||||
["quantity_type.drupal_id"],
|
|
||||||
name=op.f("fk_quantity_quantity_type_id_quantity_type"),
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["units_uuid"], ["unit.uuid"], name=op.f("fk_quantity_units_uuid_unit")
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity")),
|
|
||||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_quantity_drupal_id")),
|
|
||||||
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_quantity_farmos_uuid")),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"quantity_version",
|
|
||||||
sa.Column(
|
|
||||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"quantity_type_id", sa.String(length=50), autoincrement=False, nullable=True
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"measure_id", sa.String(length=20), autoincrement=False, nullable=True
|
|
||||||
),
|
|
||||||
sa.Column("value_numerator", sa.Integer(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"value_denominator", sa.Integer(), autoincrement=False, nullable=True
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"units_uuid",
|
|
||||||
wuttjamaican.db.util.UUID(),
|
|
||||||
autoincrement=False,
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
sa.Column("label", 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_quantity_version")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_version_end_transaction_id"),
|
|
||||||
"quantity_version",
|
|
||||||
["end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_version_operation_type"),
|
|
||||||
"quantity_version",
|
|
||||||
["operation_type"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_quantity_version_pk_transaction_id",
|
|
||||||
"quantity_version",
|
|
||||||
["uuid", sa.literal_column("transaction_id DESC")],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_quantity_version_pk_validity",
|
|
||||||
"quantity_version",
|
|
||||||
["uuid", "transaction_id", "end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_version_transaction_id"),
|
|
||||||
"quantity_version",
|
|
||||||
["transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# quantity_standard
|
|
||||||
op.create_table(
|
|
||||||
"quantity_standard",
|
|
||||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["uuid"], ["quantity.uuid"], name=op.f("fk_quantity_standard_uuid_quantity")
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity_standard")),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"quantity_standard_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_standard_version")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_standard_version_end_transaction_id"),
|
|
||||||
"quantity_standard_version",
|
|
||||||
["end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_standard_version_operation_type"),
|
|
||||||
"quantity_standard_version",
|
|
||||||
["operation_type"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_quantity_standard_version_pk_transaction_id",
|
|
||||||
"quantity_standard_version",
|
|
||||||
["uuid", sa.literal_column("transaction_id DESC")],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_quantity_standard_version_pk_validity",
|
|
||||||
"quantity_standard_version",
|
|
||||||
["uuid", "transaction_id", "end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_quantity_standard_version_transaction_id"),
|
|
||||||
"quantity_standard_version",
|
|
||||||
["transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
|
|
||||||
# quantity_standard
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_standard_version_transaction_id"),
|
|
||||||
table_name="quantity_standard_version",
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
"ix_quantity_standard_version_pk_validity",
|
|
||||||
table_name="quantity_standard_version",
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
"ix_quantity_standard_version_pk_transaction_id",
|
|
||||||
table_name="quantity_standard_version",
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_standard_version_operation_type"),
|
|
||||||
table_name="quantity_standard_version",
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_standard_version_end_transaction_id"),
|
|
||||||
table_name="quantity_standard_version",
|
|
||||||
)
|
|
||||||
op.drop_table("quantity_standard_version")
|
|
||||||
op.drop_table("quantity_standard")
|
|
||||||
|
|
||||||
# quantity
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_version_transaction_id"), table_name="quantity_version"
|
|
||||||
)
|
|
||||||
op.drop_index("ix_quantity_version_pk_validity", table_name="quantity_version")
|
|
||||||
op.drop_index(
|
|
||||||
"ix_quantity_version_pk_transaction_id", table_name="quantity_version"
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_version_operation_type"), table_name="quantity_version"
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_quantity_version_end_transaction_id"), table_name="quantity_version"
|
|
||||||
)
|
|
||||||
op.drop_table("quantity_version")
|
|
||||||
op.drop_table("quantity")
|
|
||||||
|
|
||||||
# measure
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_measure_version_transaction_id"), table_name="measure_version"
|
|
||||||
)
|
|
||||||
op.drop_index("ix_measure_version_pk_validity", table_name="measure_version")
|
|
||||||
op.drop_index("ix_measure_version_pk_transaction_id", table_name="measure_version")
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_measure_version_operation_type"), table_name="measure_version"
|
|
||||||
)
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_measure_version_end_transaction_id"), table_name="measure_version"
|
|
||||||
)
|
|
||||||
op.drop_table("measure_version")
|
|
||||||
op.drop_table("measure")
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
"""add Units
|
|
||||||
|
|
||||||
Revision ID: ea88e72a5fa5
|
|
||||||
Revises: 82a03f4ef1a4
|
|
||||||
Create Date: 2026-02-18 20:01:40.720138
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import wuttjamaican.db.util
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "ea88e72a5fa5"
|
|
||||||
down_revision: Union[str, None] = "82a03f4ef1a4"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
|
|
||||||
# unit
|
|
||||||
op.create_table(
|
|
||||||
"unit",
|
|
||||||
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_unit")),
|
|
||||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_unit_drupal_id")),
|
|
||||||
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_unit_farmos_uuid")),
|
|
||||||
sa.UniqueConstraint("name", name=op.f("uq_unit_name")),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"unit_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_unit_version")),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_unit_version_end_transaction_id"),
|
|
||||||
"unit_version",
|
|
||||||
["end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_unit_version_operation_type"),
|
|
||||||
"unit_version",
|
|
||||||
["operation_type"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_unit_version_pk_transaction_id",
|
|
||||||
"unit_version",
|
|
||||||
["uuid", sa.literal_column("transaction_id DESC")],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_unit_version_pk_validity",
|
|
||||||
"unit_version",
|
|
||||||
["uuid", "transaction_id", "end_transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_unit_version_transaction_id"),
|
|
||||||
"unit_version",
|
|
||||||
["transaction_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
|
|
||||||
# unit
|
|
||||||
op.drop_index(op.f("ix_unit_version_transaction_id"), table_name="unit_version")
|
|
||||||
op.drop_index("ix_unit_version_pk_validity", table_name="unit_version")
|
|
||||||
op.drop_index("ix_unit_version_pk_transaction_id", table_name="unit_version")
|
|
||||||
op.drop_index(op.f("ix_unit_version_operation_type"), table_name="unit_version")
|
|
||||||
op.drop_index(op.f("ix_unit_version_end_transaction_id"), table_name="unit_version")
|
|
||||||
op.drop_table("unit_version")
|
|
||||||
op.drop_table("unit")
|
|
||||||
|
|
@ -30,8 +30,6 @@ from wuttjamaican.db.model import *
|
||||||
from .users import WuttaFarmUser
|
from .users import WuttaFarmUser
|
||||||
|
|
||||||
# wuttafarm proper models
|
# wuttafarm proper models
|
||||||
from .unit import Unit, Measure
|
|
||||||
from .quantities import QuantityType, Quantity, StandardQuantity
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Model definition for Quantities
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy import orm
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
from wuttjamaican.db import model
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityType(model.Base):
|
|
||||||
"""
|
|
||||||
Represents an "quantity type" from farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "quantity_type"
|
|
||||||
__versioned__ = {}
|
|
||||||
__wutta_hint__ = {
|
|
||||||
"model_title": "Quantity Type",
|
|
||||||
"model_title_plural": "Quantity Types",
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid = model.uuid_column()
|
|
||||||
|
|
||||||
name = sa.Column(
|
|
||||||
sa.String(length=100),
|
|
||||||
nullable=False,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Name of the quantity type.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
description = sa.Column(
|
|
||||||
sa.String(length=255),
|
|
||||||
nullable=True,
|
|
||||||
doc="""
|
|
||||||
Description for the quantity type.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
farmos_uuid = sa.Column(
|
|
||||||
model.UUID(),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
UUID for the quantity type within farmOS.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
drupal_id = sa.Column(
|
|
||||||
sa.String(length=50),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Drupal internal ID for the quantity type.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name or ""
|
|
||||||
|
|
||||||
|
|
||||||
class Quantity(model.Base):
|
|
||||||
"""
|
|
||||||
Represents a base quantity record from farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "quantity"
|
|
||||||
__versioned__ = {}
|
|
||||||
__wutta_hint__ = {
|
|
||||||
"model_title": "Quantity",
|
|
||||||
"model_title_plural": "All Quantities",
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid = model.uuid_column()
|
|
||||||
|
|
||||||
quantity_type_id = sa.Column(
|
|
||||||
sa.String(length=50),
|
|
||||||
sa.ForeignKey("quantity_type.drupal_id"),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
quantity_type = orm.relationship(QuantityType)
|
|
||||||
|
|
||||||
measure_id = sa.Column(
|
|
||||||
sa.String(length=20),
|
|
||||||
sa.ForeignKey("measure.drupal_id"),
|
|
||||||
nullable=False,
|
|
||||||
doc="""
|
|
||||||
Measure for the quantity.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
measure = orm.relationship("Measure")
|
|
||||||
|
|
||||||
value_numerator = sa.Column(
|
|
||||||
sa.Integer(),
|
|
||||||
nullable=False,
|
|
||||||
doc="""
|
|
||||||
Numerator for the quantity value.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
value_denominator = sa.Column(
|
|
||||||
sa.Integer(),
|
|
||||||
nullable=False,
|
|
||||||
doc="""
|
|
||||||
Denominator for the quantity value.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
units_uuid = model.uuid_fk_column("unit.uuid", nullable=False)
|
|
||||||
units = orm.relationship("Unit")
|
|
||||||
|
|
||||||
label = sa.Column(
|
|
||||||
sa.String(length=255),
|
|
||||||
nullable=True,
|
|
||||||
doc="""
|
|
||||||
Optional label for the quantity.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
farmos_uuid = sa.Column(
|
|
||||||
model.UUID(),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
UUID for the quantity within farmOS.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
drupal_id = sa.Column(
|
|
||||||
sa.Integer(),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Drupal internal ID for the quantity.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def render_as_text(self, config=None):
|
|
||||||
measure = str(self.measure or self.measure_id or "")
|
|
||||||
value = self.value_numerator / self.value_denominator
|
|
||||||
if config:
|
|
||||||
app = config.get_app()
|
|
||||||
value = app.render_quantity(value)
|
|
||||||
units = str(self.units or "")
|
|
||||||
return f"( {measure} ) {value} {units}"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.render_as_text()
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityMixin:
|
|
||||||
|
|
||||||
uuid = model.uuid_fk_column("quantity.uuid", nullable=False, primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def quantity(cls):
|
|
||||||
return orm.relationship(Quantity)
|
|
||||||
|
|
||||||
def render_as_text(self, config=None):
|
|
||||||
return self.quantity.render_as_text(config)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.render_as_text()
|
|
||||||
|
|
||||||
|
|
||||||
def add_quantity_proxies(subclass):
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "farmos_uuid")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "drupal_id")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "quantity_type")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "quantity_type_id")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "measure")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "measure_id")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "value_numerator")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "value_denominator")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "value_decimal")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "units_uuid")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "units")
|
|
||||||
Quantity.make_proxy(subclass, "quantity", "label")
|
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantity(QuantityMixin, model.Base):
|
|
||||||
"""
|
|
||||||
Represents a Standard Quantity from farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "quantity_standard"
|
|
||||||
__versioned__ = {}
|
|
||||||
__wutta_hint__ = {
|
|
||||||
"model_title": "Standard Quantity",
|
|
||||||
"model_title_plural": "Standard Quantities",
|
|
||||||
"farmos_quantity_type": "standard",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
add_quantity_proxies(StandardQuantity)
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Model definition for Units
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from wuttjamaican.db import model
|
|
||||||
|
|
||||||
|
|
||||||
class Measure(model.Base):
|
|
||||||
"""
|
|
||||||
Represents a "measure" option (for quantities) from farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "measure"
|
|
||||||
__versioned__ = {}
|
|
||||||
__wutta_hint__ = {
|
|
||||||
"model_title": "Measure",
|
|
||||||
"model_title_plural": "Measures",
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid = model.uuid_column()
|
|
||||||
|
|
||||||
name = sa.Column(
|
|
||||||
sa.String(length=100),
|
|
||||||
nullable=False,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Name of the measure.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
drupal_id = sa.Column(
|
|
||||||
sa.String(length=20),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Drupal internal ID for the measure.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name or ""
|
|
||||||
|
|
||||||
|
|
||||||
class Unit(model.Base):
|
|
||||||
"""
|
|
||||||
Represents an "unit" (taxonomy term) from farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "unit"
|
|
||||||
__versioned__ = {}
|
|
||||||
__wutta_hint__ = {
|
|
||||||
"model_title": "Unit",
|
|
||||||
"model_title_plural": "Units",
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid = model.uuid_column()
|
|
||||||
|
|
||||||
name = sa.Column(
|
|
||||||
sa.String(length=100),
|
|
||||||
nullable=False,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Name of the unit.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
description = sa.Column(
|
|
||||||
sa.String(length=255),
|
|
||||||
nullable=True,
|
|
||||||
doc="""
|
|
||||||
Optional description for the unit.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
farmos_uuid = sa.Column(
|
|
||||||
model.UUID(),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
UUID for the unit within farmOS.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
drupal_id = sa.Column(
|
|
||||||
sa.Integer(),
|
|
||||||
nullable=True,
|
|
||||||
unique=True,
|
|
||||||
doc="""
|
|
||||||
Drupal internal ID for the unit.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name or ""
|
|
||||||
|
|
@ -28,19 +28,6 @@ from collections import OrderedDict
|
||||||
from wuttjamaican.enum import *
|
from wuttjamaican.enum import *
|
||||||
|
|
||||||
|
|
||||||
FARMOS_INTEGRATION_MODE_WRAPPER = "wrapper"
|
|
||||||
FARMOS_INTEGRATION_MODE_MIRROR = "mirror"
|
|
||||||
FARMOS_INTEGRATION_MODE_NONE = "none"
|
|
||||||
|
|
||||||
FARMOS_INTEGRATION_MODE = OrderedDict(
|
|
||||||
[
|
|
||||||
(FARMOS_INTEGRATION_MODE_WRAPPER, "wrapper (API only)"),
|
|
||||||
(FARMOS_INTEGRATION_MODE_MIRROR, "mirror (2-way sync)"),
|
|
||||||
(FARMOS_INTEGRATION_MODE_NONE, "none (standalone)"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ANIMAL_SEX = OrderedDict(
|
ANIMAL_SEX = OrderedDict(
|
||||||
[
|
[
|
||||||
("M", "Male"),
|
("M", "Male"),
|
||||||
|
|
|
||||||
|
|
@ -64,81 +64,6 @@ class ToFarmOS(Importer):
|
||||||
return self.app.make_utc(dt)
|
return self.app.make_utc(dt)
|
||||||
|
|
||||||
|
|
||||||
class ToFarmOSTaxonomy(ToFarmOS):
|
|
||||||
|
|
||||||
farmos_taxonomy_type = None
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"uuid",
|
|
||||||
"name",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_target_objects(self, **kwargs):
|
|
||||||
result = self.farmos_client.resource.get(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type
|
|
||||||
)
|
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def get_target_object(self, key):
|
|
||||||
|
|
||||||
# fetch from cache, if applicable
|
|
||||||
if self.caches_target:
|
|
||||||
return super().get_target_object(key)
|
|
||||||
|
|
||||||
# okay now must fetch via API
|
|
||||||
if self.get_keys() != ["uuid"]:
|
|
||||||
raise ValueError("must use uuid key for this to work")
|
|
||||||
uuid = key[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.farmos_client.resource.get_id(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type, str(uuid)
|
|
||||||
)
|
|
||||||
except requests.HTTPError as exc:
|
|
||||||
if exc.response.status_code == 404:
|
|
||||||
return None
|
|
||||||
raise
|
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def normalize_target_object(self, obj):
|
|
||||||
return {
|
|
||||||
"uuid": UUID(obj["id"]),
|
|
||||||
"name": obj["attributes"]["name"],
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_term_payload(self, source_data):
|
|
||||||
return {
|
|
||||||
"attributes": {
|
|
||||||
"name": source_data["name"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_target_object(self, key, source_data):
|
|
||||||
if source_data.get("__ignoreme__"):
|
|
||||||
return None
|
|
||||||
if self.dry_run:
|
|
||||||
return source_data
|
|
||||||
|
|
||||||
payload = self.get_term_payload(source_data)
|
|
||||||
result = self.farmos_client.resource.send(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type, payload
|
|
||||||
)
|
|
||||||
normal = self.normalize_target_object(result["data"])
|
|
||||||
normal["_new_object"] = result["data"]
|
|
||||||
return normal
|
|
||||||
|
|
||||||
def update_target_object(self, asset, source_data, target_data=None):
|
|
||||||
if self.dry_run:
|
|
||||||
return asset
|
|
||||||
|
|
||||||
payload = self.get_term_payload(source_data)
|
|
||||||
payload["id"] = str(source_data["uuid"])
|
|
||||||
result = self.farmos_client.resource.send(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type, payload
|
|
||||||
)
|
|
||||||
return self.normalize_target_object(result["data"])
|
|
||||||
|
|
||||||
|
|
||||||
class ToFarmOSAsset(ToFarmOS):
|
class ToFarmOSAsset(ToFarmOS):
|
||||||
"""
|
"""
|
||||||
Base class for asset data importer targeting the farmOS API.
|
Base class for asset data importer targeting the farmOS API.
|
||||||
|
|
@ -226,12 +151,6 @@ class ToFarmOSAsset(ToFarmOS):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class UnitImporter(ToFarmOSTaxonomy):
|
|
||||||
|
|
||||||
model_title = "Unit"
|
|
||||||
farmos_taxonomy_type = "unit"
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalAssetImporter(ToFarmOSAsset):
|
class AnimalAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
model_title = "AnimalAsset"
|
model_title = "AnimalAsset"
|
||||||
|
|
@ -290,10 +209,77 @@ class AnimalAssetImporter(ToFarmOSAsset):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class AnimalTypeImporter(ToFarmOSTaxonomy):
|
class AnimalTypeImporter(ToFarmOS):
|
||||||
|
|
||||||
model_title = "AnimalType"
|
model_title = "AnimalType"
|
||||||
farmos_taxonomy_type = "animal_type"
|
|
||||||
|
supported_fields = [
|
||||||
|
"uuid",
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_target_objects(self, **kwargs):
|
||||||
|
result = self.farmos_client.resource.get("taxonomy_term", "animal_type")
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
def get_target_object(self, key):
|
||||||
|
|
||||||
|
# fetch from cache, if applicable
|
||||||
|
if self.caches_target:
|
||||||
|
return super().get_target_object(key)
|
||||||
|
|
||||||
|
# okay now must fetch via API
|
||||||
|
if self.get_keys() != ["uuid"]:
|
||||||
|
raise ValueError("must use uuid key for this to work")
|
||||||
|
uuid = key[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.farmos_client.resource.get_id(
|
||||||
|
"taxonomy_term", "animal_type", str(uuid)
|
||||||
|
)
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
def normalize_target_object(self, obj):
|
||||||
|
return {
|
||||||
|
"uuid": UUID(obj["id"]),
|
||||||
|
"name": obj["attributes"]["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_type_payload(self, source_data):
|
||||||
|
return {
|
||||||
|
"attributes": {
|
||||||
|
"name": source_data["name"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_target_object(self, key, source_data):
|
||||||
|
if source_data.get("__ignoreme__"):
|
||||||
|
return None
|
||||||
|
if self.dry_run:
|
||||||
|
return source_data
|
||||||
|
|
||||||
|
payload = self.get_type_payload(source_data)
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"taxonomy_term", "animal_type", payload
|
||||||
|
)
|
||||||
|
normal = self.normalize_target_object(result["data"])
|
||||||
|
normal["_new_object"] = result["data"]
|
||||||
|
return normal
|
||||||
|
|
||||||
|
def update_target_object(self, asset, source_data, target_data=None):
|
||||||
|
if self.dry_run:
|
||||||
|
return asset
|
||||||
|
|
||||||
|
payload = self.get_type_payload(source_data)
|
||||||
|
payload["id"] = str(source_data["uuid"])
|
||||||
|
result = self.farmos_client.resource.send(
|
||||||
|
"taxonomy_term", "animal_type", payload
|
||||||
|
)
|
||||||
|
return self.normalize_target_object(result["data"])
|
||||||
|
|
||||||
|
|
||||||
class GroupAssetImporter(ToFarmOSAsset):
|
class GroupAssetImporter(ToFarmOSAsset):
|
||||||
|
|
@ -347,59 +333,6 @@ class LandAssetImporter(ToFarmOSAsset):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetImporter(ToFarmOSAsset):
|
|
||||||
|
|
||||||
model_title = "PlantAsset"
|
|
||||||
farmos_asset_type = "plant"
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"uuid",
|
|
||||||
"asset_name",
|
|
||||||
"plant_type_uuids",
|
|
||||||
"notes",
|
|
||||||
"archived",
|
|
||||||
]
|
|
||||||
|
|
||||||
def normalize_target_object(self, plant):
|
|
||||||
data = super().normalize_target_object(plant)
|
|
||||||
data.update(
|
|
||||||
{
|
|
||||||
"plant_type_uuids": [
|
|
||||||
UUID(p["id"]) for p in plant["relationships"]["plant_type"]["data"]
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_asset_payload(self, source_data):
|
|
||||||
payload = super().get_asset_payload(source_data)
|
|
||||||
|
|
||||||
attrs = {}
|
|
||||||
if "sex" in self.fields:
|
|
||||||
attrs["sex"] = source_data["sex"]
|
|
||||||
if "is_sterile" in self.fields:
|
|
||||||
attrs["is_sterile"] = source_data["is_sterile"]
|
|
||||||
if "birthdate" in self.fields:
|
|
||||||
attrs["birthdate"] = self.format_datetime(source_data["birthdate"])
|
|
||||||
|
|
||||||
rels = {}
|
|
||||||
if "plant_type_uuids" in self.fields:
|
|
||||||
rels["plant_type"] = {"data": []}
|
|
||||||
for uuid in source_data["plant_type_uuids"]:
|
|
||||||
rels["plant_type"]["data"].append(
|
|
||||||
{
|
|
||||||
"id": str(uuid),
|
|
||||||
"type": "taxonomy_term--plant_type",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
payload["attributes"].update(attrs)
|
|
||||||
if rels:
|
|
||||||
payload.setdefault("relationships", {}).update(rels)
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
class StructureAssetImporter(ToFarmOSAsset):
|
class StructureAssetImporter(ToFarmOSAsset):
|
||||||
|
|
||||||
model_title = "StructureAsset"
|
model_title = "StructureAsset"
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,6 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
||||||
importers["AnimalType"] = AnimalTypeImporter
|
importers["AnimalType"] = AnimalTypeImporter
|
||||||
importers["AnimalAsset"] = AnimalAssetImporter
|
importers["AnimalAsset"] = AnimalAssetImporter
|
||||||
importers["GroupAsset"] = GroupAssetImporter
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
|
||||||
importers["Unit"] = UnitImporter
|
|
||||||
importers["ActivityLog"] = ActivityLogImporter
|
importers["ActivityLog"] = ActivityLogImporter
|
||||||
importers["HarvestLog"] = HarvestLogImporter
|
importers["HarvestLog"] = HarvestLogImporter
|
||||||
importers["MedicalLog"] = MedicalLogImporter
|
importers["MedicalLog"] = MedicalLogImporter
|
||||||
|
|
@ -185,28 +183,6 @@ class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter):
|
|
||||||
"""
|
|
||||||
WuttaFarm → farmOS API exporter for Units
|
|
||||||
"""
|
|
||||||
|
|
||||||
source_model_class = model.Unit
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"uuid",
|
|
||||||
"name",
|
|
||||||
]
|
|
||||||
|
|
||||||
drupal_internal_id_field = "drupal_internal__tid"
|
|
||||||
|
|
||||||
def normalize_source_object(self, unit):
|
|
||||||
return {
|
|
||||||
"uuid": unit.farmos_uuid or self.app.make_true_uuid(),
|
|
||||||
"name": unit.name,
|
|
||||||
"_src_object": unit,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter):
|
class GroupAssetImporter(FromWuttaFarm, farmos_importing.model.GroupAssetImporter):
|
||||||
"""
|
"""
|
||||||
WuttaFarm → farmOS API exporter for Group Assets
|
WuttaFarm → farmOS API exporter for Group Assets
|
||||||
|
|
@ -263,32 +239,6 @@ class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlantAssetImporter(FromWuttaFarm, farmos_importing.model.PlantAssetImporter):
|
|
||||||
"""
|
|
||||||
WuttaFarm → farmOS API exporter for Plant Assets
|
|
||||||
"""
|
|
||||||
|
|
||||||
source_model_class = model.PlantAsset
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"uuid",
|
|
||||||
"asset_name",
|
|
||||||
"plant_type_uuids",
|
|
||||||
"notes",
|
|
||||||
"archived",
|
|
||||||
]
|
|
||||||
|
|
||||||
def normalize_source_object(self, plant):
|
|
||||||
return {
|
|
||||||
"uuid": plant.farmos_uuid or self.app.make_true_uuid(),
|
|
||||||
"asset_name": plant.asset_name,
|
|
||||||
"plant_type_uuids": [t.plant_type.farmos_uuid for t in plant._plant_types],
|
|
||||||
"notes": plant.notes,
|
|
||||||
"archived": plant.archived,
|
|
||||||
"_src_object": plant,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StructureAssetImporter(
|
class StructureAssetImporter(
|
||||||
FromWuttaFarm, farmos_importing.model.StructureAssetImporter
|
FromWuttaFarm, farmos_importing.model.StructureAssetImporter
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ class FromFarmOSHandler(ImportHandler):
|
||||||
token = self.get_farmos_oauth2_token()
|
token = self.get_farmos_oauth2_token()
|
||||||
self.farmos_client = self.app.get_farmos_client(token=token)
|
self.farmos_client = self.app.get_farmos_client(token=token)
|
||||||
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
||||||
self.normal = self.app.get_normalizer(self.farmos_client)
|
|
||||||
|
|
||||||
def get_farmos_oauth2_token(self):
|
def get_farmos_oauth2_token(self):
|
||||||
|
|
||||||
|
|
@ -77,7 +76,6 @@ class FromFarmOSHandler(ImportHandler):
|
||||||
kwargs = super().get_importer_kwargs(key, **kwargs)
|
kwargs = super().get_importer_kwargs(key, **kwargs)
|
||||||
kwargs["farmos_client"] = self.farmos_client
|
kwargs["farmos_client"] = self.farmos_client
|
||||||
kwargs["farmos_4x"] = self.farmos_4x
|
kwargs["farmos_4x"] = self.farmos_4x
|
||||||
kwargs["normal"] = self.normal
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -108,10 +106,6 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
|
||||||
importers["GroupAsset"] = GroupAssetImporter
|
importers["GroupAsset"] = GroupAssetImporter
|
||||||
importers["PlantType"] = PlantTypeImporter
|
importers["PlantType"] = PlantTypeImporter
|
||||||
importers["PlantAsset"] = PlantAssetImporter
|
importers["PlantAsset"] = PlantAssetImporter
|
||||||
importers["Measure"] = MeasureImporter
|
|
||||||
importers["Unit"] = UnitImporter
|
|
||||||
importers["QuantityType"] = QuantityTypeImporter
|
|
||||||
importers["StandardQuantity"] = StandardQuantityImporter
|
|
||||||
importers["LogType"] = LogTypeImporter
|
importers["LogType"] = LogTypeImporter
|
||||||
importers["ActivityLog"] = ActivityLogImporter
|
importers["ActivityLog"] = ActivityLogImporter
|
||||||
importers["HarvestLog"] = HarvestLogImporter
|
importers["HarvestLog"] = HarvestLogImporter
|
||||||
|
|
@ -827,95 +821,6 @@ class UserImporter(FromFarmOS, ToWutta):
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
|
||||||
class MeasureImporter(FromFarmOS, ToWutta):
|
|
||||||
"""
|
|
||||||
farmOS API → WuttaFarm importer for Measures
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = model.Measure
|
|
||||||
|
|
||||||
key = "drupal_id"
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"drupal_id",
|
|
||||||
"name",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_source_objects(self):
|
|
||||||
""" """
|
|
||||||
response = self.farmos_client.session.get(
|
|
||||||
self.app.get_farmos_url("/api/quantity/standard/resource/schema")
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
return data["definitions"]["attributes"]["properties"]["measure"]["oneOf"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, measure):
|
|
||||||
""" """
|
|
||||||
return {
|
|
||||||
"drupal_id": measure["const"],
|
|
||||||
"name": measure["title"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UnitImporter(FromFarmOS, ToWutta):
|
|
||||||
"""
|
|
||||||
farmOS API → WuttaFarm importer for Units
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = model.Unit
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_source_objects(self):
|
|
||||||
""" """
|
|
||||||
result = self.farmos_client.resource.get("taxonomy_term", "unit")
|
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, unit):
|
|
||||||
""" """
|
|
||||||
return {
|
|
||||||
"farmos_uuid": UUID(unit["id"]),
|
|
||||||
"drupal_id": unit["attributes"]["drupal_internal__tid"],
|
|
||||||
"name": unit["attributes"]["name"],
|
|
||||||
"description": unit["attributes"]["description"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityTypeImporter(FromFarmOS, ToWutta):
|
|
||||||
"""
|
|
||||||
farmOS API → WuttaFarm importer for Quantity Types
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = model.QuantityType
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_source_objects(self):
|
|
||||||
""" """
|
|
||||||
result = self.farmos_client.resource.get("quantity_type")
|
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def normalize_source_object(self, quantity_type):
|
|
||||||
""" """
|
|
||||||
return {
|
|
||||||
"farmos_uuid": UUID(quantity_type["id"]),
|
|
||||||
"drupal_id": quantity_type["attributes"]["drupal_internal__id"],
|
|
||||||
"name": quantity_type["attributes"]["label"],
|
|
||||||
"description": quantity_type["attributes"]["description"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LogTypeImporter(FromFarmOS, ToWutta):
|
class LogTypeImporter(FromFarmOS, ToWutta):
|
||||||
"""
|
"""
|
||||||
farmOS API → WuttaFarm importer for Log Types
|
farmOS API → WuttaFarm importer for Log Types
|
||||||
|
|
@ -983,25 +888,33 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
def get_source_objects(self):
|
def get_source_objects(self):
|
||||||
""" """
|
""" """
|
||||||
log_type = self.get_farmos_log_type()
|
log_type = self.get_farmos_log_type()
|
||||||
return list(self.farmos_client.log.iterate(log_type))
|
result = self.farmos_client.log.get(log_type)
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
def get_asset_type(self, asset):
|
||||||
|
return asset["type"].split("--")[1]
|
||||||
|
|
||||||
def normalize_source_object(self, log):
|
def normalize_source_object(self, log):
|
||||||
""" """
|
""" """
|
||||||
data = self.normal.normalize_farmos_log(log)
|
if notes := log["attributes"]["notes"]:
|
||||||
|
notes = notes["value"]
|
||||||
data["farmos_uuid"] = UUID(data.pop("uuid"))
|
|
||||||
data["message"] = data.pop("name")
|
|
||||||
data["timestamp"] = self.app.make_utc(data["timestamp"])
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
data["log_type"] = self.get_farmos_log_type()
|
|
||||||
|
|
||||||
|
assets = None
|
||||||
if "assets" in self.fields:
|
if "assets" in self.fields:
|
||||||
data["assets"] = [
|
assets = []
|
||||||
(a["asset_type"], UUID(a["uuid"])) for a in data["assets"]
|
for asset in log["relationships"]["asset"]["data"]:
|
||||||
]
|
assets.append((self.get_asset_type(asset), UUID(asset["id"])))
|
||||||
|
|
||||||
return data
|
return {
|
||||||
|
"farmos_uuid": UUID(log["id"]),
|
||||||
|
"drupal_id": log["attributes"]["drupal_internal__id"],
|
||||||
|
"log_type": self.get_farmos_log_type(),
|
||||||
|
"message": log["attributes"]["name"],
|
||||||
|
"timestamp": self.normalize_datetime(log["attributes"]["timestamp"]),
|
||||||
|
"notes": notes,
|
||||||
|
"status": log["attributes"]["status"],
|
||||||
|
"assets": assets,
|
||||||
|
}
|
||||||
|
|
||||||
def normalize_target_object(self, log):
|
def normalize_target_object(self, log):
|
||||||
data = super().normalize_target_object(log)
|
data = super().normalize_target_object(log)
|
||||||
|
|
@ -1127,134 +1040,3 @@ class ObservationLogImporter(LogImporterBase):
|
||||||
"status",
|
"status",
|
||||||
"assets",
|
"assets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class QuantityImporterBase(FromFarmOS, ToWutta):
|
|
||||||
"""
|
|
||||||
Base class for farmOS API → WuttaFarm quantity importers
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_farmos_quantity_type(self):
|
|
||||||
return self.model_class.__wutta_hint__["farmos_quantity_type"]
|
|
||||||
|
|
||||||
def get_simple_fields(self):
|
|
||||||
""" """
|
|
||||||
fields = list(super().get_simple_fields())
|
|
||||||
# nb. must explicitly declare proxy fields
|
|
||||||
fields.extend(
|
|
||||||
[
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
"quantity_type_id",
|
|
||||||
"measure_id",
|
|
||||||
"value_numerator",
|
|
||||||
"value_denominator",
|
|
||||||
"units_uuid",
|
|
||||||
"label",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
super().setup()
|
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
self.quantity_types_by_farmos_uuid = {}
|
|
||||||
for quantity_type in self.target_session.query(model.QuantityType):
|
|
||||||
if quantity_type.farmos_uuid:
|
|
||||||
self.quantity_types_by_farmos_uuid[quantity_type.farmos_uuid] = (
|
|
||||||
quantity_type
|
|
||||||
)
|
|
||||||
|
|
||||||
self.units_by_farmos_uuid = {}
|
|
||||||
for unit in self.target_session.query(model.Unit):
|
|
||||||
if unit.farmos_uuid:
|
|
||||||
self.units_by_farmos_uuid[unit.farmos_uuid] = unit
|
|
||||||
|
|
||||||
def get_source_objects(self):
|
|
||||||
""" """
|
|
||||||
quantity_type = self.get_farmos_quantity_type()
|
|
||||||
result = self.farmos_client.resource.get("quantity", quantity_type)
|
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
def get_quantity_type_by_farmos_uuid(self, uuid):
|
|
||||||
if hasattr(self, "quantity_types_by_farmos_uuid"):
|
|
||||||
return self.quantity_types_by_farmos_uuid.get(UUID(uuid))
|
|
||||||
|
|
||||||
model = self.app.model
|
|
||||||
return (
|
|
||||||
self.target_session.query(model.QuantityType)
|
|
||||||
.filter(model.QuantityType.farmos_uuid == uuid)
|
|
||||||
.one()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_unit_by_farmos_uuid(self, uuid):
|
|
||||||
if hasattr(self, "units_by_farmos_uuid"):
|
|
||||||
return self.units_by_farmos_uuid.get(UUID(uuid))
|
|
||||||
|
|
||||||
model = self.app.model
|
|
||||||
return (
|
|
||||||
self.target_session.query(model.Unit)
|
|
||||||
.filter(model.Unit.farmos_uuid == uuid)
|
|
||||||
.one()
|
|
||||||
)
|
|
||||||
|
|
||||||
def normalize_source_object(self, quantity):
|
|
||||||
""" """
|
|
||||||
quantity_type_id = None
|
|
||||||
units_uuid = None
|
|
||||||
if relationships := quantity.get("relationships"):
|
|
||||||
|
|
||||||
if quantity_type := relationships.get("quantity_type"):
|
|
||||||
if quantity_type["data"]:
|
|
||||||
if wf_quantity_type := self.get_quantity_type_by_farmos_uuid(
|
|
||||||
quantity_type["data"]["id"]
|
|
||||||
):
|
|
||||||
quantity_type_id = wf_quantity_type.drupal_id
|
|
||||||
|
|
||||||
if units := relationships.get("units"):
|
|
||||||
if units["data"]:
|
|
||||||
if wf_unit := self.get_unit_by_farmos_uuid(units["data"]["id"]):
|
|
||||||
units_uuid = wf_unit.uuid
|
|
||||||
|
|
||||||
if not quantity_type_id:
|
|
||||||
log.warning(
|
|
||||||
"missing/invalid quantity_type for farmOS Quantity: %s", quantity
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not units_uuid:
|
|
||||||
log.warning("missing/invalid units for farmOS Quantity: %s", quantity)
|
|
||||||
return None
|
|
||||||
|
|
||||||
value = quantity["attributes"]["value"]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"farmos_uuid": UUID(quantity["id"]),
|
|
||||||
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
|
||||||
"quantity_type_id": quantity_type_id,
|
|
||||||
"measure_id": quantity["attributes"]["measure"],
|
|
||||||
"value_numerator": value["numerator"],
|
|
||||||
"value_denominator": value["denominator"],
|
|
||||||
"units_uuid": units_uuid,
|
|
||||||
"label": quantity["attributes"]["label"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantityImporter(QuantityImporterBase):
|
|
||||||
"""
|
|
||||||
farmOS API → WuttaFarm importer for Standard Quantities
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = model.StandardQuantity
|
|
||||||
|
|
||||||
supported_fields = [
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
"quantity_type_id",
|
|
||||||
"measure_id",
|
|
||||||
"value_numerator",
|
|
||||||
"value_denominator",
|
|
||||||
"units_uuid",
|
|
||||||
"label",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Data normalizer for WuttaFarm / farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from wuttjamaican.app import GenericHandler
|
|
||||||
|
|
||||||
|
|
||||||
class Normalizer(GenericHandler):
|
|
||||||
"""
|
|
||||||
Base class and default implementation for the global data
|
|
||||||
normalizer. This should be used for normalizing records from
|
|
||||||
WuttaFarm and/or farmOS.
|
|
||||||
|
|
||||||
The point here is to have a single place to put the normalization
|
|
||||||
logic, and let it be another thing which can be customized via
|
|
||||||
subclass.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_farmos_units = None
|
|
||||||
_farmos_measures = None
|
|
||||||
|
|
||||||
def __init__(self, config, farmos_client=None):
|
|
||||||
super().__init__(config)
|
|
||||||
self.farmos_client = farmos_client
|
|
||||||
|
|
||||||
def get_farmos_measures(self):
|
|
||||||
if self._farmos_measures:
|
|
||||||
return self._farmos_measures
|
|
||||||
|
|
||||||
measures = {}
|
|
||||||
response = self.farmos_client.session.get(
|
|
||||||
self.app.get_farmos_url("/api/quantity/standard/resource/schema")
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
for measure in data["definitions"]["attributes"]["properties"]["measure"][
|
|
||||||
"oneOf"
|
|
||||||
]:
|
|
||||||
measures[measure["const"]] = measure["title"]
|
|
||||||
|
|
||||||
self._farmos_measures = measures
|
|
||||||
return self._farmos_measures
|
|
||||||
|
|
||||||
def get_farmos_measure_name(self, measure_id):
|
|
||||||
measures = self.get_farmos_measures()
|
|
||||||
return measures[measure_id]
|
|
||||||
|
|
||||||
def get_farmos_unit(self, uuid):
|
|
||||||
units = self.get_farmos_units()
|
|
||||||
return units[uuid]
|
|
||||||
|
|
||||||
def get_farmos_units(self):
|
|
||||||
if self._farmos_units:
|
|
||||||
return self._farmos_units
|
|
||||||
|
|
||||||
units = {}
|
|
||||||
result = self.farmos_client.resource.get("taxonomy_term", "unit")
|
|
||||||
for unit in result["data"]:
|
|
||||||
units[unit["id"]] = unit
|
|
||||||
|
|
||||||
self._farmos_units = units
|
|
||||||
return self._farmos_units
|
|
||||||
|
|
||||||
def normalize_farmos_log(self, log, included={}):
|
|
||||||
|
|
||||||
if timestamp := log["attributes"]["timestamp"]:
|
|
||||||
timestamp = datetime.datetime.fromisoformat(timestamp)
|
|
||||||
timestamp = self.app.localtime(timestamp)
|
|
||||||
|
|
||||||
if notes := log["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
log_type_object = {}
|
|
||||||
log_type_uuid = None
|
|
||||||
asset_objects = []
|
|
||||||
quantity_objects = []
|
|
||||||
quantity_uuids = []
|
|
||||||
owner_objects = []
|
|
||||||
owner_uuids = []
|
|
||||||
if relationships := log.get("relationships"):
|
|
||||||
|
|
||||||
if log_type := relationships.get("log_type"):
|
|
||||||
log_type_uuid = log_type["data"]["id"]
|
|
||||||
if log_type := included.get(log_type_uuid):
|
|
||||||
log_type_object = {
|
|
||||||
"uuid": log_type["id"],
|
|
||||||
"name": log_type["attributes"]["label"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if assets := relationships.get("asset"):
|
|
||||||
for asset in assets["data"]:
|
|
||||||
asset_object = {
|
|
||||||
"uuid": asset["id"],
|
|
||||||
"type": asset["type"],
|
|
||||||
"asset_type": asset["type"].split("--")[1],
|
|
||||||
}
|
|
||||||
if asset := included.get(asset["id"]):
|
|
||||||
attrs = asset["attributes"]
|
|
||||||
rels = asset["relationships"]
|
|
||||||
asset_object.update(
|
|
||||||
{
|
|
||||||
"drupal_id": attrs["drupal_internal__id"],
|
|
||||||
"name": attrs["name"],
|
|
||||||
"is_location": attrs["is_location"],
|
|
||||||
"is_fixed": attrs["is_fixed"],
|
|
||||||
"archived": attrs["archived"],
|
|
||||||
"notes": attrs["notes"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
asset_objects.append(asset_object)
|
|
||||||
|
|
||||||
if quantities := relationships.get("quantity"):
|
|
||||||
for quantity in quantities["data"]:
|
|
||||||
quantity_uuid = quantity["id"]
|
|
||||||
quantity_uuids.append(quantity_uuid)
|
|
||||||
if quantity := included.get(quantity_uuid):
|
|
||||||
attrs = quantity["attributes"]
|
|
||||||
rels = quantity["relationships"]
|
|
||||||
value = attrs["value"]
|
|
||||||
|
|
||||||
unit_uuid = rels["units"]["data"]["id"]
|
|
||||||
unit = self.get_farmos_unit(unit_uuid)
|
|
||||||
|
|
||||||
measure_id = attrs["measure"]
|
|
||||||
|
|
||||||
quantity_objects.append(
|
|
||||||
{
|
|
||||||
"uuid": quantity["id"],
|
|
||||||
"drupal_id": attrs["drupal_internal__id"],
|
|
||||||
"quantity_type_uuid": rels["quantity_type"]["data"][
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"quantity_type_id": rels["quantity_type"]["data"][
|
|
||||||
"meta"
|
|
||||||
]["drupal_internal__target_id"],
|
|
||||||
"measure_id": measure_id,
|
|
||||||
"measure_name": self.get_farmos_measure_name(
|
|
||||||
measure_id
|
|
||||||
),
|
|
||||||
"value_numerator": value["numerator"],
|
|
||||||
"value_decimal": value["decimal"],
|
|
||||||
"value_denominator": value["denominator"],
|
|
||||||
"unit_uuid": unit_uuid,
|
|
||||||
"unit_name": unit["attributes"]["name"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if owners := relationships.get("owner"):
|
|
||||||
for user in owners["data"]:
|
|
||||||
user_uuid = user["id"]
|
|
||||||
owner_uuids.append(user_uuid)
|
|
||||||
if user := included.get(user_uuid):
|
|
||||||
owner_objects.append(
|
|
||||||
{
|
|
||||||
"uuid": user["id"],
|
|
||||||
"name": user["attributes"]["name"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"uuid": log["id"],
|
|
||||||
"drupal_id": log["attributes"]["drupal_internal__id"],
|
|
||||||
"log_type_uuid": log_type_uuid,
|
|
||||||
"log_type": log_type_object,
|
|
||||||
"name": log["attributes"]["name"],
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"assets": asset_objects,
|
|
||||||
"quantities": quantity_objects,
|
|
||||||
"quantity_uuids": quantity_uuids,
|
|
||||||
"is_group_assignment": log["attributes"]["is_group_assignment"],
|
|
||||||
"quick": log["attributes"]["quick"],
|
|
||||||
"status": log["attributes"]["status"],
|
|
||||||
"notes": notes,
|
|
||||||
"owners": owner_objects,
|
|
||||||
"owner_uuids": owner_uuids,
|
|
||||||
}
|
|
||||||
|
|
@ -55,135 +55,6 @@ class AnimalTypeRef(ObjectRef):
|
||||||
return self.request.route_url("animal_types.view", uuid=animal_type.uuid)
|
return self.request.route_url("animal_types.view", uuid=animal_type.uuid)
|
||||||
|
|
||||||
|
|
||||||
class LogQuick(WuttaSet):
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import LogQuickWidget
|
|
||||||
|
|
||||||
return LogQuickWidget(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSUnitRef(colander.SchemaType):
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import FarmOSUnitRefWidget
|
|
||||||
|
|
||||||
return FarmOSUnitRefWidget(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSRef(colander.SchemaType):
|
|
||||||
|
|
||||||
def __init__(self, request, route_prefix, *args, **kwargs):
|
|
||||||
self.values = kwargs.pop("values", None)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.request = request
|
|
||||||
self.route_prefix = route_prefix
|
|
||||||
|
|
||||||
def get_values(self):
|
|
||||||
if callable(self.values):
|
|
||||||
self.values = self.values()
|
|
||||||
return self.values
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
# nb. keep a ref to this for later use
|
|
||||||
node.model_instance = appstruct
|
|
||||||
|
|
||||||
# serialize to PK as string
|
|
||||||
return appstruct["uuid"]
|
|
||||||
|
|
||||||
def deserialize(self, node, cstruct):
|
|
||||||
if not cstruct:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
# nb. deserialize to PK string, not dict
|
|
||||||
return cstruct
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import FarmOSRefWidget
|
|
||||||
|
|
||||||
if not kwargs.get("readonly"):
|
|
||||||
if "values" not in kwargs:
|
|
||||||
if values := self.get_values():
|
|
||||||
kwargs["values"] = values
|
|
||||||
|
|
||||||
return FarmOSRefWidget(self.request, self.route_prefix, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSRefs(WuttaSet):
|
|
||||||
|
|
||||||
def __init__(self, request, route_prefix, *args, **kwargs):
|
|
||||||
super().__init__(request, *args, **kwargs)
|
|
||||||
self.route_prefix = route_prefix
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import FarmOSRefsWidget
|
|
||||||
|
|
||||||
return FarmOSRefsWidget(self.request, self.route_prefix, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSAssetRefs(WuttaSet):
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import FarmOSAssetRefsWidget
|
|
||||||
|
|
||||||
return FarmOSAssetRefsWidget(self.request, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSLocationRefs(WuttaSet):
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import FarmOSLocationRefsWidget
|
|
||||||
|
|
||||||
return FarmOSLocationRefsWidget(self.request, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSQuantityRefs(WuttaSet):
|
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
|
||||||
if appstruct is colander.null:
|
|
||||||
return colander.null
|
|
||||||
|
|
||||||
return json.dumps(appstruct)
|
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
|
||||||
from wuttafarm.web.forms.widgets import FarmOSQuantityRefsWidget
|
|
||||||
|
|
||||||
return FarmOSQuantityRefsWidget(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalTypeType(colander.SchemaType):
|
class AnimalTypeType(colander.SchemaType):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
|
@ -308,27 +179,6 @@ class StructureTypeRef(ObjectRef):
|
||||||
return self.request.route_url("structure_types.view", uuid=structure_type.uuid)
|
return self.request.route_url("structure_types.view", uuid=structure_type.uuid)
|
||||||
|
|
||||||
|
|
||||||
class UnitRef(ObjectRef):
|
|
||||||
"""
|
|
||||||
Custom schema type for a :class:`~wuttafarm.db.model.units.Unit`
|
|
||||||
reference field.
|
|
||||||
|
|
||||||
This is a subclass of
|
|
||||||
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def model_class(self):
|
|
||||||
model = self.app.model
|
|
||||||
return model.Unit
|
|
||||||
|
|
||||||
def sort_query(self, query):
|
|
||||||
return query.order_by(self.model_class.name)
|
|
||||||
|
|
||||||
def get_object_url(self, unit):
|
|
||||||
return self.request.route_url("units.view", uuid=unit.uuid)
|
|
||||||
|
|
||||||
|
|
||||||
class UsersType(colander.SchemaType):
|
class UsersType(colander.SchemaType):
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,12 @@ Custom form widgets for WuttaFarm
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform.widget import Widget, SelectWidget
|
from deform.widget import Widget
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
|
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
from wuttafarm.web.util import render_quantity_objects
|
|
||||||
|
|
||||||
|
|
||||||
class ImageWidget(Widget):
|
class ImageWidget(Widget):
|
||||||
"""
|
"""
|
||||||
|
|
@ -56,172 +54,6 @@ class ImageWidget(Widget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
class LogQuickWidget(Widget):
|
|
||||||
"""
|
|
||||||
Widget to display an image URL for a record.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
|
||||||
""" """
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
|
||||||
if readonly:
|
|
||||||
if cstruct in (colander.null, None):
|
|
||||||
return HTML.tag("span")
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for quick in json.loads(cstruct):
|
|
||||||
items.append(HTML.tag("li", c=quick))
|
|
||||||
return HTML.tag("ul", c=items)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSRefWidget(SelectWidget):
|
|
||||||
"""
|
|
||||||
Generic widget to display "any reference field" - as a link to
|
|
||||||
view the farmOS record it references. Only used by the farmOS
|
|
||||||
direct API views.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
try:
|
|
||||||
obj = json.loads(cstruct)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
name = dict(self.values)[cstruct]
|
|
||||||
obj = {"uuid": cstruct, "name": name}
|
|
||||||
|
|
||||||
return tags.link_to(
|
|
||||||
obj["name"],
|
|
||||||
self.request.route_url(f"{self.route_prefix}.view", uuid=obj["uuid"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSRefsWidget(Widget):
|
|
||||||
|
|
||||||
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 obj in json.loads(cstruct):
|
|
||||||
url = self.request.route_url(
|
|
||||||
f"{self.route_prefix}.view", uuid=obj["uuid"]
|
|
||||||
)
|
|
||||||
links.append(HTML.tag("li", c=tags.link_to(obj["name"], url)))
|
|
||||||
|
|
||||||
return HTML.tag("ul", c=links)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSAssetRefsWidget(Widget):
|
|
||||||
"""
|
|
||||||
Widget to display a "Assets" field for an asset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
|
||||||
if readonly:
|
|
||||||
if cstruct in (colander.null, None):
|
|
||||||
return HTML.tag("span")
|
|
||||||
|
|
||||||
assets = []
|
|
||||||
for asset in json.loads(cstruct):
|
|
||||||
url = self.request.route_url(
|
|
||||||
f"farmos_{asset['asset_type']}_assets.view", uuid=asset["uuid"]
|
|
||||||
)
|
|
||||||
assets.append(HTML.tag("li", c=tags.link_to(asset["name"], url)))
|
|
||||||
|
|
||||||
return HTML.tag("ul", c=assets)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSLocationRefsWidget(Widget):
|
|
||||||
"""
|
|
||||||
Widget to display a "Locations" field for an asset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
|
||||||
if readonly:
|
|
||||||
if cstruct in (colander.null, None):
|
|
||||||
return HTML.tag("span")
|
|
||||||
|
|
||||||
locations = []
|
|
||||||
for location in json.loads(cstruct):
|
|
||||||
asset_type = location["type"].split("--")[1]
|
|
||||||
url = self.request.route_url(
|
|
||||||
f"farmos_{asset_type}_assets.view", uuid=location["uuid"]
|
|
||||||
)
|
|
||||||
locations.append(HTML.tag("li", c=tags.link_to(location["name"], url)))
|
|
||||||
|
|
||||||
return HTML.tag("ul", c=locations)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSQuantityRefsWidget(Widget):
|
|
||||||
"""
|
|
||||||
Widget to display a "Quantities" field for a log.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
|
||||||
if readonly:
|
|
||||||
if cstruct in (colander.null, None):
|
|
||||||
return HTML.tag("span")
|
|
||||||
|
|
||||||
quantities = json.loads(cstruct)
|
|
||||||
return render_quantity_objects(quantities)
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSUnitRefWidget(Widget):
|
|
||||||
"""
|
|
||||||
Widget to display a "Units" field for a quantity.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
|
||||||
readonly = kw.get("readonly", self.readonly)
|
|
||||||
if readonly:
|
|
||||||
if cstruct in (colander.null, None):
|
|
||||||
return HTML.tag("span")
|
|
||||||
|
|
||||||
unit = json.loads(cstruct)
|
|
||||||
return unit["name"]
|
|
||||||
|
|
||||||
return super().serialize(field, cstruct, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalTypeWidget(Widget):
|
class AnimalTypeWidget(Widget):
|
||||||
"""
|
"""
|
||||||
Widget to display an "animal type" field.
|
Widget to display an "animal type" field.
|
||||||
|
|
@ -330,7 +162,7 @@ class StructureWidget(Widget):
|
||||||
return tags.link_to(
|
return tags.link_to(
|
||||||
structure["name"],
|
structure["name"],
|
||||||
self.request.route_url(
|
self.request.route_url(
|
||||||
"farmos_structure_assets.view", uuid=structure["uuid"]
|
"farmos_structures.view", uuid=structure["uuid"]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Custom grid stuff for use with farmOS / JSONAPI
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from wuttaweb.grids.filters import GridFilter
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleFilter(GridFilter):
|
|
||||||
|
|
||||||
default_verbs = ["equal", "not_equal"]
|
|
||||||
|
|
||||||
def __init__(self, request, key, path=None, **kwargs):
|
|
||||||
super().__init__(request, key, **kwargs)
|
|
||||||
self.path = path or key
|
|
||||||
|
|
||||||
def filter_equal(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, "=", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_not_equal(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, "<>", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_is_null(self, data, value):
|
|
||||||
data.add_filter(self.path, "IS NULL", None)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_is_not_null(self, data, value):
|
|
||||||
data.add_filter(self.path, "IS NOT NULL", None)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class StringFilter(SimpleFilter):
|
|
||||||
|
|
||||||
default_verbs = ["contains", "equal", "not_equal"]
|
|
||||||
|
|
||||||
def filter_contains(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, "CONTAINS", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class NullableStringFilter(StringFilter):
|
|
||||||
|
|
||||||
default_verbs = ["contains", "equal", "not_equal", "is_null", "is_not_null"]
|
|
||||||
|
|
||||||
|
|
||||||
class IntegerFilter(SimpleFilter):
|
|
||||||
|
|
||||||
default_verbs = [
|
|
||||||
"equal",
|
|
||||||
"not_equal",
|
|
||||||
"less_than",
|
|
||||||
"less_equal",
|
|
||||||
"greater_than",
|
|
||||||
"greater_equal",
|
|
||||||
]
|
|
||||||
|
|
||||||
def filter_less_than(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, "<", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_less_equal(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, "<=", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_greater_than(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, ">", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_greater_equal(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
data.add_filter(self.path, ">=", value)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class NullableIntegerFilter(IntegerFilter):
|
|
||||||
|
|
||||||
default_verbs = ["equal", "not_equal", "is_null", "is_not_null"]
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanFilter(SimpleFilter):
|
|
||||||
|
|
||||||
default_verbs = ["is_true", "is_false"]
|
|
||||||
|
|
||||||
def filter_is_true(self, data, value):
|
|
||||||
data.add_filter(self.path, "=", 1)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def filter_is_false(self, data, value):
|
|
||||||
data.add_filter(self.path, "=", 0)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class NullableBooleanFilter(BooleanFilter):
|
|
||||||
|
|
||||||
default_verbs = ["is_true", "is_false", "is_null", "is_not_null"]
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: this may not work, it's not used anywhere yet
|
|
||||||
class DateFilter(SimpleFilter):
|
|
||||||
|
|
||||||
data_type = "date"
|
|
||||||
|
|
||||||
default_verbs = [
|
|
||||||
"equal",
|
|
||||||
"not_equal",
|
|
||||||
"greater_than",
|
|
||||||
"greater_equal",
|
|
||||||
"less_than",
|
|
||||||
"less_equal",
|
|
||||||
# 'between',
|
|
||||||
]
|
|
||||||
|
|
||||||
default_verb_labels = {
|
|
||||||
"equal": "on",
|
|
||||||
"not_equal": "not on",
|
|
||||||
"greater_than": "after",
|
|
||||||
"greater_equal": "on or after",
|
|
||||||
"less_than": "before",
|
|
||||||
"less_equal": "on or before",
|
|
||||||
# "between": "between",
|
|
||||||
"is_null": "is null",
|
|
||||||
"is_not_null": "is not null",
|
|
||||||
"is_any": "is any",
|
|
||||||
}
|
|
||||||
|
|
||||||
def coerce_value(self, value):
|
|
||||||
if value:
|
|
||||||
if isinstance(value, datetime.date):
|
|
||||||
return value
|
|
||||||
|
|
||||||
try:
|
|
||||||
dt = datetime.datetime.strptime(value, "%Y-%m-%d")
|
|
||||||
except ValueError:
|
|
||||||
log.warning("invalid date value: %s", value)
|
|
||||||
else:
|
|
||||||
return dt.date()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: this is not very complete yet, so far used only for animal birthdate
|
|
||||||
class DateTimeFilter(DateFilter):
|
|
||||||
|
|
||||||
default_verbs = ["equal", "is_null", "is_not_null"]
|
|
||||||
|
|
||||||
def coerce_value(self, value):
|
|
||||||
"""
|
|
||||||
Convert user input to a proper ``datetime.date`` object.
|
|
||||||
"""
|
|
||||||
if value:
|
|
||||||
if isinstance(value, datetime.date):
|
|
||||||
return value
|
|
||||||
|
|
||||||
try:
|
|
||||||
dt = datetime.datetime.strptime(value, "%Y-%m-%d")
|
|
||||||
except ValueError:
|
|
||||||
log.warning("invalid date value: %s", value)
|
|
||||||
else:
|
|
||||||
return dt.date()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def filter_equal(self, data, value):
|
|
||||||
if value := self.coerce_value(value):
|
|
||||||
|
|
||||||
start = datetime.datetime.combine(value, datetime.time(0))
|
|
||||||
start = self.app.localtime(start, from_utc=False)
|
|
||||||
|
|
||||||
stop = datetime.datetime.combine(
|
|
||||||
value + datetime.timedelta(days=1), datetime.time(0)
|
|
||||||
)
|
|
||||||
stop = self.app.localtime(stop, from_utc=False)
|
|
||||||
|
|
||||||
data.add_filter(self.path, ">=", int(start.timestamp()))
|
|
||||||
data.add_filter(self.path, "<", int(stop.timestamp()))
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleSorter:
|
|
||||||
|
|
||||||
def __init__(self, key):
|
|
||||||
self.key = key
|
|
||||||
|
|
||||||
def __call__(self, data, sortdir):
|
|
||||||
data.add_sorter(self.key, sortdir)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceData:
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config,
|
|
||||||
farmos_client,
|
|
||||||
content_type,
|
|
||||||
include=None,
|
|
||||||
normalizer=None,
|
|
||||||
):
|
|
||||||
self.config = config
|
|
||||||
self.farmos_client = farmos_client
|
|
||||||
self.entity, self.bundle = content_type.split("--")
|
|
||||||
self.filters = []
|
|
||||||
self.sorters = []
|
|
||||||
self.include = include
|
|
||||||
self.normalizer = normalizer
|
|
||||||
self._data = None
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __getitem__(self, subscript):
|
|
||||||
return self.get_data()[subscript]
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._data)
|
|
||||||
|
|
||||||
def add_filter(self, path, operator, value):
|
|
||||||
self.filters.append((path, operator, value))
|
|
||||||
|
|
||||||
def add_sorter(self, path, sortdir):
|
|
||||||
self.sorters.append((path, sortdir))
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
if self._data is None:
|
|
||||||
params = {}
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
for path, operator, value in self.filters:
|
|
||||||
i += 1
|
|
||||||
key = f"{i:03d}"
|
|
||||||
params[f"filter[{key}][condition][path]"] = path
|
|
||||||
params[f"filter[{key}][condition][operator]"] = operator
|
|
||||||
params[f"filter[{key}][condition][value]"] = value
|
|
||||||
|
|
||||||
sorters = []
|
|
||||||
for path, sortdir in self.sorters:
|
|
||||||
prefix = "-" if sortdir == "desc" else ""
|
|
||||||
sorters.append(f"{prefix}{path}")
|
|
||||||
if sorters:
|
|
||||||
params["sort"] = ",".join(sorters)
|
|
||||||
|
|
||||||
# nb. while the API allows for pagination, it does not
|
|
||||||
# tell me how many total records there are (IIUC). also
|
|
||||||
# if i ask for e.g. items 21-40 (page 2 @ 20/page) i am
|
|
||||||
# not guaranteed to get 20 items even if there are plenty
|
|
||||||
# in the DB, since Drupal may filter some out based on
|
|
||||||
# permissions. (granted that may not be an issue in
|
|
||||||
# practice, but can't rule it out.) so the punchline is,
|
|
||||||
# we fetch "all" (sic) data and send it to the frontend,
|
|
||||||
# and pagination happens there.
|
|
||||||
|
|
||||||
# TODO: if we ever try again, this sort of works...
|
|
||||||
# params["page[offset]"] = start
|
|
||||||
# params["page[limit]"] = stop - start
|
|
||||||
|
|
||||||
if self.include:
|
|
||||||
params["include"] = self.include
|
|
||||||
|
|
||||||
result = self.farmos_client.resource.get(
|
|
||||||
self.entity, self.bundle, params=params
|
|
||||||
)
|
|
||||||
data = result["data"]
|
|
||||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
|
||||||
|
|
||||||
if self.normalizer:
|
|
||||||
data = [self.normalizer(d, included) for d in data]
|
|
||||||
|
|
||||||
self._data = data
|
|
||||||
return self._data
|
|
||||||
|
|
@ -32,51 +32,13 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def make_menus(self, request, **kwargs):
|
def make_menus(self, request, **kwargs):
|
||||||
enum = self.app.enum
|
|
||||||
mode = self.app.get_farmos_integration_mode()
|
|
||||||
|
|
||||||
quick_menu = self.make_quick_menu(request)
|
|
||||||
admin_menu = self.make_admin_menu(request, include_people=True)
|
|
||||||
|
|
||||||
if mode == enum.FARMOS_INTEGRATION_MODE_WRAPPER:
|
|
||||||
return [
|
return [
|
||||||
quick_menu,
|
|
||||||
self.make_farmos_asset_menu(request),
|
|
||||||
self.make_farmos_log_menu(request),
|
|
||||||
self.make_farmos_other_menu(request),
|
|
||||||
admin_menu,
|
|
||||||
]
|
|
||||||
|
|
||||||
elif mode == enum.FARMOS_INTEGRATION_MODE_MIRROR:
|
|
||||||
return [
|
|
||||||
quick_menu,
|
|
||||||
self.make_asset_menu(request),
|
self.make_asset_menu(request),
|
||||||
self.make_log_menu(request),
|
self.make_log_menu(request),
|
||||||
self.make_farmos_full_menu(request),
|
self.make_farmos_menu(request),
|
||||||
admin_menu,
|
self.make_admin_menu(request, include_people=True),
|
||||||
]
|
]
|
||||||
|
|
||||||
else: # FARMOS_INTEGRATION_MODE_NONE
|
|
||||||
return [
|
|
||||||
quick_menu,
|
|
||||||
self.make_asset_menu(request),
|
|
||||||
self.make_log_menu(request),
|
|
||||||
admin_menu,
|
|
||||||
]
|
|
||||||
|
|
||||||
def make_quick_menu(self, request):
|
|
||||||
return {
|
|
||||||
"title": "Quick",
|
|
||||||
"type": "menu",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Eggs",
|
|
||||||
"route": "quick.eggs",
|
|
||||||
# "perm": "assets.list",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_asset_menu(self, request):
|
def make_asset_menu(self, request):
|
||||||
return {
|
return {
|
||||||
"title": "Assets",
|
"title": "Assets",
|
||||||
|
|
@ -172,41 +134,15 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"perm": "logs_observation.list",
|
"perm": "logs_observation.list",
|
||||||
},
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
|
||||||
"title": "All Quantities",
|
|
||||||
"route": "quantities",
|
|
||||||
"perm": "quantities.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Standard Quantities",
|
|
||||||
"route": "quantities_standard",
|
|
||||||
"perm": "quantities_standard.list",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
|
||||||
{
|
{
|
||||||
"title": "Log Types",
|
"title": "Log Types",
|
||||||
"route": "log_types",
|
"route": "log_types",
|
||||||
"perm": "log_types.list",
|
"perm": "log_types.list",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Measures",
|
|
||||||
"route": "measures",
|
|
||||||
"perm": "measures.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Quantity Types",
|
|
||||||
"route": "quantity_types",
|
|
||||||
"perm": "quantity_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Units",
|
|
||||||
"route": "units",
|
|
||||||
"perm": "units.list",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def make_farmos_full_menu(self, request):
|
def make_farmos_menu(self, request):
|
||||||
config = request.wutta_config
|
config = request.wutta_config
|
||||||
app = config.get_app()
|
app = config.get_app()
|
||||||
return {
|
return {
|
||||||
|
|
@ -220,30 +156,30 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
},
|
},
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Animal Assets",
|
"title": "Animals",
|
||||||
"route": "farmos_animal_assets",
|
"route": "farmos_animals",
|
||||||
"perm": "farmos_animal_assets.list",
|
"perm": "farmos_animals.list",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Group Assets",
|
"title": "Groups",
|
||||||
"route": "farmos_group_assets",
|
"route": "farmos_groups",
|
||||||
"perm": "farmos_group_assets.list",
|
"perm": "farmos_groups.list",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Land Assets",
|
"title": "Plants",
|
||||||
|
"route": "farmos_asset_plant",
|
||||||
|
"perm": "farmos_asset_plant.list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Structures",
|
||||||
|
"route": "farmos_structures",
|
||||||
|
"perm": "farmos_structures.list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Land",
|
||||||
"route": "farmos_land_assets",
|
"route": "farmos_land_assets",
|
||||||
"perm": "farmos_land_assets.list",
|
"perm": "farmos_land_assets.list",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Plant Assets",
|
|
||||||
"route": "farmos_plant_assets",
|
|
||||||
"perm": "farmos_plant_assets.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Structure Assets",
|
|
||||||
"route": "farmos_structure_assets",
|
|
||||||
"perm": "farmos_structure_assets.list",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Activity Logs",
|
"title": "Activity Logs",
|
||||||
|
|
@ -271,11 +207,6 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_animal_types",
|
"route": "farmos_animal_types",
|
||||||
"perm": "farmos_animal_types.list",
|
"perm": "farmos_animal_types.list",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Land Types",
|
|
||||||
"route": "farmos_land_types",
|
|
||||||
"perm": "farmos_land_types.list",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "Plant Types",
|
"title": "Plant Types",
|
||||||
"route": "farmos_plant_types",
|
"route": "farmos_plant_types",
|
||||||
|
|
@ -286,7 +217,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_structure_types",
|
"route": "farmos_structure_types",
|
||||||
"perm": "farmos_structure_types.list",
|
"perm": "farmos_structure_types.list",
|
||||||
},
|
},
|
||||||
{"type": "sep"},
|
{
|
||||||
|
"title": "Land Types",
|
||||||
|
"route": "farmos_land_types",
|
||||||
|
"perm": "farmos_land_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Asset Types",
|
"title": "Asset Types",
|
||||||
"route": "farmos_asset_types",
|
"route": "farmos_asset_types",
|
||||||
|
|
@ -297,155 +232,6 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "farmos_log_types",
|
"route": "farmos_log_types",
|
||||||
"perm": "farmos_log_types.list",
|
"perm": "farmos_log_types.list",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Quantity Types",
|
|
||||||
"route": "farmos_quantity_types",
|
|
||||||
"perm": "farmos_quantity_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Standard Quantities",
|
|
||||||
"route": "farmos_quantities_standard",
|
|
||||||
"perm": "farmos_quantities_standard.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Units",
|
|
||||||
"route": "farmos_units",
|
|
||||||
"perm": "farmos_units.list",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
|
||||||
{
|
|
||||||
"title": "Users",
|
|
||||||
"route": "farmos_users",
|
|
||||||
"perm": "farmos_users.list",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_farmos_asset_menu(self, request):
|
|
||||||
config = request.wutta_config
|
|
||||||
app = config.get_app()
|
|
||||||
return {
|
|
||||||
"title": "Assets",
|
|
||||||
"type": "menu",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Animal",
|
|
||||||
"route": "farmos_animal_assets",
|
|
||||||
"perm": "farmos_animal_assets.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Group",
|
|
||||||
"route": "farmos_group_assets",
|
|
||||||
"perm": "farmos_group_assets.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Land",
|
|
||||||
"route": "farmos_land_assets",
|
|
||||||
"perm": "farmos_land_assets.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Plant",
|
|
||||||
"route": "farmos_plant_assets",
|
|
||||||
"perm": "farmos_plant_assets.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Structure",
|
|
||||||
"route": "farmos_structure_assets",
|
|
||||||
"perm": "farmos_structure_assets.list",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
|
||||||
{
|
|
||||||
"title": "Animal Types",
|
|
||||||
"route": "farmos_animal_types",
|
|
||||||
"perm": "farmos_animal_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Land Types",
|
|
||||||
"route": "farmos_land_types",
|
|
||||||
"perm": "farmos_land_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Plant Types",
|
|
||||||
"route": "farmos_plant_types",
|
|
||||||
"perm": "farmos_plant_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Structure Types",
|
|
||||||
"route": "farmos_structure_types",
|
|
||||||
"perm": "farmos_structure_types.list",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
|
||||||
{
|
|
||||||
"title": "Asset Types",
|
|
||||||
"route": "farmos_asset_types",
|
|
||||||
"perm": "farmos_asset_types.list",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_farmos_log_menu(self, request):
|
|
||||||
config = request.wutta_config
|
|
||||||
app = config.get_app()
|
|
||||||
return {
|
|
||||||
"title": "Logs",
|
|
||||||
"type": "menu",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Activity",
|
|
||||||
"route": "farmos_logs_activity",
|
|
||||||
"perm": "farmos_logs_activity.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Harvest",
|
|
||||||
"route": "farmos_logs_harvest",
|
|
||||||
"perm": "farmos_logs_harvest.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Medical",
|
|
||||||
"route": "farmos_logs_medical",
|
|
||||||
"perm": "farmos_logs_medical.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Observation",
|
|
||||||
"route": "farmos_logs_observation",
|
|
||||||
"perm": "farmos_logs_observation.list",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
|
||||||
{
|
|
||||||
"title": "Log Types",
|
|
||||||
"route": "farmos_log_types",
|
|
||||||
"perm": "farmos_log_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Quantity Types",
|
|
||||||
"route": "farmos_quantity_types",
|
|
||||||
"perm": "farmos_quantity_types.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Standard Quantities",
|
|
||||||
"route": "farmos_quantities_standard",
|
|
||||||
"perm": "farmos_quantities_standard.list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Units",
|
|
||||||
"route": "farmos_units",
|
|
||||||
"perm": "farmos_units.list",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_farmos_other_menu(self, request):
|
|
||||||
config = request.wutta_config
|
|
||||||
app = config.get_app()
|
|
||||||
return {
|
|
||||||
"title": "farmOS",
|
|
||||||
"type": "menu",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Go to farmOS",
|
|
||||||
"url": app.get_farmos_url(),
|
|
||||||
"target": "_blank",
|
|
||||||
},
|
|
||||||
{"type": "sep"},
|
{"type": "sep"},
|
||||||
{
|
{
|
||||||
"title": "Users",
|
"title": "Users",
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
<%inherit file="wuttaweb:templates/appinfo/configure.mako" />
|
|
||||||
|
|
||||||
<%def name="form_content()">
|
|
||||||
${parent.form_content()}
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">farmOS</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem; width: 50%;">
|
|
||||||
|
|
||||||
<b-field label="farmOS URL">
|
|
||||||
<b-input name="farmos.url.base"
|
|
||||||
v-model="simpleSettings['farmos.url.base']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="farmOS Integration Mode">
|
|
||||||
<b-select name="${app.appname}.farmos_integration_mode"
|
|
||||||
v-model="simpleSettings['${app.appname}.farmos_integration_mode']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
% for value, label in enum.FARMOS_INTEGRATION_MODE.items():
|
|
||||||
<option value="${value}">${label}</option>
|
|
||||||
% endfor
|
|
||||||
</b-select>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-checkbox name="${app.appname}.farmos_style_grid_links"
|
|
||||||
v-model="simpleSettings['${app.appname}.farmos_style_grid_links']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Use farmOS-style grid links
|
|
||||||
</b-checkbox>
|
|
||||||
<${b}-tooltip position="${'right' if request.use_oruga else 'is-right'}">
|
|
||||||
<b-icon pack="fas" icon="info-circle" />
|
|
||||||
<template #content>
|
|
||||||
<p class="block">
|
|
||||||
If set, certain column values in a grid may link
|
|
||||||
to <span class="has-text-weight-bold">related</span>
|
|
||||||
records.
|
|
||||||
</p>
|
|
||||||
<p class="block">
|
|
||||||
If not set, column values will only link to view the
|
|
||||||
<span class="has-text-weight-bold">current</span> record.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</${b}-tooltip>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</%def>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<%inherit file="/form.mako" />
|
|
||||||
|
|
||||||
<%def name="title()">${index_title} » ${form_title}</%def>
|
|
||||||
|
|
||||||
<%def name="content_title()">${form_title}</%def>
|
|
||||||
|
|
||||||
<%def name="render_form_tag()">
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
${help_text}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
${parent.render_form_tag()}
|
|
||||||
</%def>
|
|
||||||
|
|
@ -23,8 +23,6 @@
|
||||||
Misc. utilities for web app
|
Misc. utilities for web app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from webhelpers2.html import HTML
|
|
||||||
|
|
||||||
|
|
||||||
def save_farmos_oauth2_token(request, token):
|
def save_farmos_oauth2_token(request, token):
|
||||||
"""
|
"""
|
||||||
|
|
@ -40,22 +38,3 @@ def save_farmos_oauth2_token(request, token):
|
||||||
|
|
||||||
# save token to user session
|
# save token to user session
|
||||||
request.session["farmos.oauth2.token"] = token
|
request.session["farmos.oauth2.token"] = token
|
||||||
|
|
||||||
|
|
||||||
def use_farmos_style_grid_links(config):
|
|
||||||
return config.get_bool(f"{config.appname}.farmos_style_grid_links", default=True)
|
|
||||||
|
|
||||||
|
|
||||||
def render_quantity_objects(quantities):
|
|
||||||
items = []
|
|
||||||
for quantity in quantities:
|
|
||||||
text = render_quantity_object(quantity)
|
|
||||||
items.append(HTML.tag("li", c=text))
|
|
||||||
return HTML.tag("ul", c=items)
|
|
||||||
|
|
||||||
|
|
||||||
def render_quantity_object(quantity):
|
|
||||||
measure = quantity["measure_name"]
|
|
||||||
value = quantity["value_decimal"]
|
|
||||||
unit = quantity["unit_name"]
|
|
||||||
return f"( {measure} ) {value} {unit}"
|
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,6 @@ from .master import WuttaFarmMasterView
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
wutta_config = config.registry.settings.get("wutta_config")
|
|
||||||
app = wutta_config.get_app()
|
|
||||||
enum = app.enum
|
|
||||||
mode = app.get_farmos_integration_mode()
|
|
||||||
|
|
||||||
# wuttaweb core
|
# wuttaweb core
|
||||||
essential.defaults(
|
essential.defaults(
|
||||||
|
|
@ -40,15 +36,11 @@ def includeme(config):
|
||||||
**{
|
**{
|
||||||
"wuttaweb.views.auth": "wuttafarm.web.views.auth",
|
"wuttaweb.views.auth": "wuttafarm.web.views.auth",
|
||||||
"wuttaweb.views.common": "wuttafarm.web.views.common",
|
"wuttaweb.views.common": "wuttafarm.web.views.common",
|
||||||
"wuttaweb.views.settings": "wuttafarm.web.views.settings",
|
|
||||||
"wuttaweb.views.users": "wuttafarm.web.views.users",
|
"wuttaweb.views.users": "wuttafarm.web.views.users",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# native table views
|
# native table views
|
||||||
if mode != enum.FARMOS_INTEGRATION_MODE_WRAPPER:
|
|
||||||
config.include("wuttafarm.web.views.units")
|
|
||||||
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")
|
||||||
|
|
@ -62,10 +54,5 @@ def includeme(config):
|
||||||
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")
|
||||||
|
|
||||||
# quick form views
|
|
||||||
# (nb. these work with all integration modes)
|
|
||||||
config.include("wuttafarm.web.views.quick")
|
|
||||||
|
|
||||||
# views for farmOS
|
# views for farmOS
|
||||||
if mode != enum.FARMOS_INTEGRATION_MODE_NONE:
|
|
||||||
config.include("wuttafarm.web.views.farmos")
|
config.include("wuttafarm.web.views.farmos")
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@
|
||||||
Master view for Animals
|
Master view for Animals
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDictEnum
|
from wuttaweb.forms.schema import WuttaDictEnum
|
||||||
|
|
||||||
from wuttafarm.db.model import AnimalType, AnimalAsset
|
from wuttafarm.db.model import AnimalType, AnimalAsset
|
||||||
|
|
@ -155,11 +153,11 @@ class AnimalAssetView(AssetMasterView):
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"produces_eggs",
|
|
||||||
"animal_type",
|
"animal_type",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"is_sterile",
|
"is_sterile",
|
||||||
"sex",
|
"sex",
|
||||||
|
"produces_eggs",
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -167,9 +165,9 @@ class AnimalAssetView(AssetMasterView):
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"animal_type",
|
"animal_type",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"produces_eggs",
|
|
||||||
"sex",
|
"sex",
|
||||||
"is_sterile",
|
"is_sterile",
|
||||||
|
"produces_eggs",
|
||||||
"notes",
|
"notes",
|
||||||
"asset_type",
|
"asset_type",
|
||||||
"archived",
|
"archived",
|
||||||
|
|
@ -191,10 +189,6 @@ class AnimalAssetView(AssetMasterView):
|
||||||
g.set_joiner("animal_type", lambda q: q.join(model.AnimalType))
|
g.set_joiner("animal_type", lambda q: q.join(model.AnimalType))
|
||||||
g.set_sorter("animal_type", model.AnimalType.name)
|
g.set_sorter("animal_type", model.AnimalType.name)
|
||||||
g.set_filter("animal_type", model.AnimalType.name)
|
g.set_filter("animal_type", model.AnimalType.name)
|
||||||
if self.farmos_style_grid_links:
|
|
||||||
g.set_renderer("animal_type", self.render_animal_type_for_grid)
|
|
||||||
else:
|
|
||||||
g.set_link("animal_type")
|
|
||||||
|
|
||||||
# birthdate
|
# birthdate
|
||||||
g.set_renderer("birthdate", "date")
|
g.set_renderer("birthdate", "date")
|
||||||
|
|
@ -202,10 +196,6 @@ class AnimalAssetView(AssetMasterView):
|
||||||
# sex
|
# sex
|
||||||
g.set_enum("sex", enum.ANIMAL_SEX)
|
g.set_enum("sex", enum.ANIMAL_SEX)
|
||||||
|
|
||||||
def render_animal_type_for_grid(self, animal, field, value):
|
|
||||||
url = self.request.route_url("animal_types.view", uuid=animal.animal_type_uuid)
|
|
||||||
return tags.link_to(value, url)
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
f = form
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
|
||||||
|
|
@ -278,12 +278,27 @@ class AssetMasterView(WuttaFarmMasterView):
|
||||||
buttons = super().get_xref_buttons(asset)
|
buttons = super().get_xref_buttons(asset)
|
||||||
|
|
||||||
if asset.farmos_uuid:
|
if asset.farmos_uuid:
|
||||||
asset_type = self.get_model_class().__wutta_hint__["farmos_asset_type"]
|
|
||||||
route = f"farmos_{asset_type}_assets.view"
|
# TODO
|
||||||
url = self.request.route_url(route, uuid=asset.farmos_uuid)
|
route = None
|
||||||
|
if asset.asset_type == "animal":
|
||||||
|
route = "farmos_animals.view"
|
||||||
|
elif asset.asset_type == "group":
|
||||||
|
route = "farmos_groups.view"
|
||||||
|
elif asset.asset_type == "land":
|
||||||
|
route = "farmos_land_assets.view"
|
||||||
|
elif asset.asset_type == "plant":
|
||||||
|
route = "farmos_asset_plant.view"
|
||||||
|
elif asset.asset_type == "structure":
|
||||||
|
route = "farmos_structures.view"
|
||||||
|
|
||||||
|
if route:
|
||||||
buttons.append(
|
buttons.append(
|
||||||
self.make_button(
|
self.make_button(
|
||||||
"View farmOS record", primary=True, url=url, icon_left="eye"
|
"View farmOS record",
|
||||||
|
primary=True,
|
||||||
|
url=self.request.route_url(route, uuid=asset.farmos_uuid),
|
||||||
|
icon_left="eye",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ class CommonView(base.CommonView):
|
||||||
site_admin = session.query(model.Role).filter_by(name="Site Admin").first()
|
site_admin = session.query(model.Role).filter_by(name="Site Admin").first()
|
||||||
if site_admin:
|
if site_admin:
|
||||||
site_admin_perms = [
|
site_admin_perms = [
|
||||||
|
"activity_logs.list",
|
||||||
|
"activity_logs.view",
|
||||||
|
"activity_logs.versions",
|
||||||
"animal_types.create",
|
"animal_types.create",
|
||||||
"animal_types.edit",
|
"animal_types.edit",
|
||||||
"animal_types.list",
|
"animal_types.list",
|
||||||
|
|
@ -65,14 +68,14 @@ class CommonView(base.CommonView):
|
||||||
"asset_types.list",
|
"asset_types.list",
|
||||||
"asset_types.view",
|
"asset_types.view",
|
||||||
"asset_types.versions",
|
"asset_types.versions",
|
||||||
"farmos_animal_assets.list",
|
|
||||||
"farmos_animal_assets.view",
|
|
||||||
"farmos_animal_types.list",
|
"farmos_animal_types.list",
|
||||||
"farmos_animal_types.view",
|
"farmos_animal_types.view",
|
||||||
|
"farmos_animals.list",
|
||||||
|
"farmos_animals.view",
|
||||||
"farmos_asset_types.list",
|
"farmos_asset_types.list",
|
||||||
"farmos_asset_types.view",
|
"farmos_asset_types.view",
|
||||||
"farmos_group_assets.list",
|
"farmos_groups.list",
|
||||||
"farmos_group_assets.view",
|
"farmos_groups.view",
|
||||||
"farmos_land_assets.list",
|
"farmos_land_assets.list",
|
||||||
"farmos_land_assets.view",
|
"farmos_land_assets.view",
|
||||||
"farmos_land_types.list",
|
"farmos_land_types.list",
|
||||||
|
|
@ -81,23 +84,17 @@ class CommonView(base.CommonView):
|
||||||
"farmos_log_types.view",
|
"farmos_log_types.view",
|
||||||
"farmos_logs_activity.list",
|
"farmos_logs_activity.list",
|
||||||
"farmos_logs_activity.view",
|
"farmos_logs_activity.view",
|
||||||
"farmos_logs_harvest.list",
|
|
||||||
"farmos_logs_harvest.view",
|
|
||||||
"farmos_logs_medical.list",
|
|
||||||
"farmos_logs_medical.view",
|
|
||||||
"farmos_logs_observation.list",
|
|
||||||
"farmos_logs_observation.view",
|
|
||||||
"farmos_structure_assets.list",
|
|
||||||
"farmos_structure_assets.view",
|
|
||||||
"farmos_structure_types.list",
|
"farmos_structure_types.list",
|
||||||
"farmos_structure_types.view",
|
"farmos_structure_types.view",
|
||||||
|
"farmos_structures.list",
|
||||||
|
"farmos_structures.view",
|
||||||
"farmos_users.list",
|
"farmos_users.list",
|
||||||
"farmos_users.view",
|
"farmos_users.view",
|
||||||
"group_assets.create",
|
"group_asests.create",
|
||||||
"group_assets.edit",
|
"group_asests.edit",
|
||||||
"group_assets.list",
|
"group_asests.list",
|
||||||
"group_assets.view",
|
"group_asests.view",
|
||||||
"group_assets.versions",
|
"group_asests.versions",
|
||||||
"land_assets.create",
|
"land_assets.create",
|
||||||
"land_assets.edit",
|
"land_assets.edit",
|
||||||
"land_assets.list",
|
"land_assets.list",
|
||||||
|
|
@ -109,18 +106,6 @@ class CommonView(base.CommonView):
|
||||||
"log_types.list",
|
"log_types.list",
|
||||||
"log_types.view",
|
"log_types.view",
|
||||||
"log_types.versions",
|
"log_types.versions",
|
||||||
"logs_activity.list",
|
|
||||||
"logs_activity.view",
|
|
||||||
"logs_activity.versions",
|
|
||||||
"logs_harvest.list",
|
|
||||||
"logs_harvest.view",
|
|
||||||
"logs_harvest.versions",
|
|
||||||
"logs_medical.list",
|
|
||||||
"logs_medical.view",
|
|
||||||
"logs_medical.versions",
|
|
||||||
"logs_observation.list",
|
|
||||||
"logs_observation.view",
|
|
||||||
"logs_observation.versions",
|
|
||||||
"structure_types.list",
|
"structure_types.list",
|
||||||
"structure_types.view",
|
"structure_types.view",
|
||||||
"structure_types.versions",
|
"structure_types.versions",
|
||||||
|
|
@ -129,11 +114,6 @@ class CommonView(base.CommonView):
|
||||||
"structure_assets.list",
|
"structure_assets.list",
|
||||||
"structure_assets.view",
|
"structure_assets.view",
|
||||||
"structure_assets.versions",
|
"structure_assets.versions",
|
||||||
"units.create",
|
|
||||||
"units.edit",
|
|
||||||
"units.list",
|
|
||||||
"units.view",
|
|
||||||
"units.versions",
|
|
||||||
]
|
]
|
||||||
for perm in site_admin_perms:
|
for perm in site_admin_perms:
|
||||||
auth.grant_permission(site_admin, perm)
|
auth.grant_permission(site_admin, perm)
|
||||||
|
|
|
||||||
|
|
@ -28,9 +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.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.land_types")
|
config.include("wuttafarm.web.views.farmos.land_types")
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,16 @@
|
||||||
View for farmOS animal types
|
View for farmOS animal types
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
import datetime
|
||||||
|
|
||||||
|
import colander
|
||||||
|
|
||||||
|
from wuttaweb.forms.schema import WuttaDateTime
|
||||||
|
|
||||||
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
|
|
||||||
|
|
||||||
class AnimalTypeView(TaxonomyMasterView):
|
class AnimalTypeView(FarmOSMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Animal Types in farmOS.
|
Master view for Animal Types in farmOS.
|
||||||
"""
|
"""
|
||||||
|
|
@ -38,14 +44,90 @@ class AnimalTypeView(TaxonomyMasterView):
|
||||||
route_prefix = "farmos_animal_types"
|
route_prefix = "farmos_animal_types"
|
||||||
url_prefix = "/farmOS/animal-types"
|
url_prefix = "/farmOS/animal-types"
|
||||||
|
|
||||||
farmos_taxonomy_type = "animal_type"
|
|
||||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
|
farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"changed",
|
||||||
|
]
|
||||||
|
|
||||||
|
sort_defaults = "name"
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"changed",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_grid_data(self, columns=None, session=None):
|
||||||
|
animal_types = self.farmos_client.resource.get("taxonomy_term", "animal_type")
|
||||||
|
return [self.normalize_animal_type(t) for t in animal_types["data"]]
|
||||||
|
|
||||||
|
def configure_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# name
|
||||||
|
g.set_link("name")
|
||||||
|
g.set_searchable("name")
|
||||||
|
|
||||||
|
# changed
|
||||||
|
g.set_renderer("changed", "datetime")
|
||||||
|
|
||||||
|
def get_instance(self):
|
||||||
|
animal_type = self.farmos_client.resource.get_id(
|
||||||
|
"taxonomy_term", "animal_type", self.request.matchdict["uuid"]
|
||||||
|
)
|
||||||
|
self.raw_json = animal_type
|
||||||
|
return self.normalize_animal_type(animal_type["data"])
|
||||||
|
|
||||||
|
def get_instance_title(self, animal_type):
|
||||||
|
return animal_type["name"]
|
||||||
|
|
||||||
|
def normalize_animal_type(self, animal_type):
|
||||||
|
|
||||||
|
if changed := animal_type["attributes"]["changed"]:
|
||||||
|
changed = datetime.datetime.fromisoformat(changed)
|
||||||
|
changed = self.app.localtime(changed)
|
||||||
|
|
||||||
|
if description := animal_type["attributes"]["description"]:
|
||||||
|
description = description["value"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uuid": animal_type["id"],
|
||||||
|
"drupal_id": animal_type["attributes"]["drupal_internal__tid"],
|
||||||
|
"name": animal_type["attributes"]["name"],
|
||||||
|
"description": description or colander.null,
|
||||||
|
"changed": changed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure_form(self, form):
|
||||||
|
f = form
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# description
|
||||||
|
f.set_widget("description", "notes")
|
||||||
|
|
||||||
|
# changed
|
||||||
|
f.set_node("changed", WuttaDateTime())
|
||||||
|
|
||||||
def get_xref_buttons(self, animal_type):
|
def get_xref_buttons(self, animal_type):
|
||||||
buttons = super().get_xref_buttons(animal_type)
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
self.make_button(
|
||||||
|
"View in farmOS",
|
||||||
|
primary=True,
|
||||||
|
url=self.app.get_farmos_url(
|
||||||
|
f"/taxonomy/term/{animal_type['drupal_id']}"
|
||||||
|
),
|
||||||
|
target="_blank",
|
||||||
|
icon_left="external-link-alt",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
if wf_animal_type := (
|
if wf_animal_type := (
|
||||||
session.query(model.AnimalType)
|
session.query(model.AnimalType)
|
||||||
.filter(model.AnimalType.farmos_uuid == animal_type["uuid"])
|
.filter(model.AnimalType.farmos_uuid == animal_type["uuid"])
|
||||||
|
|
|
||||||
|
|
@ -26,145 +26,94 @@ Master view for Farm Animals
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum
|
from wuttaweb.forms.schema import WuttaDateTime
|
||||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos.assets import AssetMasterView
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
from wuttafarm.web.grids import (
|
from wuttafarm.web.forms.schema import UsersType, AnimalTypeType, StructureType
|
||||||
SimpleSorter,
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
StringFilter,
|
|
||||||
BooleanFilter,
|
|
||||||
NullableBooleanFilter,
|
|
||||||
DateTimeFilter,
|
|
||||||
)
|
|
||||||
from wuttafarm.web.forms.schema import FarmOSRef
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalView(AssetMasterView):
|
class AnimalView(FarmOSMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Farm Animals
|
Master view for Farm Animals
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_name = "farmos_animal_assets"
|
model_name = "farmos_animal"
|
||||||
model_title = "farmOS Animal Asset"
|
model_title = "farmOS Animal"
|
||||||
model_title_plural = "farmOS Animal Assets"
|
model_title_plural = "farmOS Animals"
|
||||||
|
|
||||||
route_prefix = "farmos_animal_assets"
|
route_prefix = "farmos_animals"
|
||||||
url_prefix = "/farmOS/assets/animal"
|
url_prefix = "/farmOS/animals"
|
||||||
|
|
||||||
farmos_asset_type = "animal"
|
|
||||||
farmos_refurl_path = "/assets/animal"
|
farmos_refurl_path = "/assets/animal"
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"animal_type": "Species / Breed",
|
"animal_type": "Species / Breed",
|
||||||
"animal_type_name": "Species / Breed",
|
"location": "Current Location",
|
||||||
"is_sterile": "Sterile",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"thumbnail",
|
|
||||||
"drupal_id",
|
|
||||||
"name",
|
"name",
|
||||||
"produces_eggs",
|
|
||||||
"animal_type_name",
|
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"is_sterile",
|
|
||||||
"sex",
|
"sex",
|
||||||
"groups",
|
"is_sterile",
|
||||||
"owners",
|
|
||||||
"locations",
|
|
||||||
"archived",
|
"archived",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
sort_defaults = "name"
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"animal_type",
|
"animal_type",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"produces_eggs",
|
|
||||||
"sex",
|
"sex",
|
||||||
"is_sterile",
|
"is_sterile",
|
||||||
"notes",
|
|
||||||
"asset_type_name",
|
|
||||||
"groups",
|
|
||||||
"owners",
|
|
||||||
"locations",
|
|
||||||
"archived",
|
"archived",
|
||||||
"thumbnail_url",
|
"owners",
|
||||||
"image_url",
|
"location",
|
||||||
"thumbnail",
|
"notes",
|
||||||
|
"raw_image_url",
|
||||||
|
"large_image_url",
|
||||||
|
"thumbnail_image_url",
|
||||||
"image",
|
"image",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_farmos_api_includes(self):
|
def get_grid_data(self, columns=None, session=None):
|
||||||
includes = super().get_farmos_api_includes()
|
animals = self.farmos_client.resource.get("asset", "animal")
|
||||||
includes.add("animal_type")
|
return [self.normalize_animal(a) for a in animals["data"]]
|
||||||
includes.add("group")
|
|
||||||
return includes
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
enum = self.app.enum
|
|
||||||
|
|
||||||
# produces_eggs
|
# name
|
||||||
g.set_renderer("produces_eggs", "boolean")
|
g.set_link("name")
|
||||||
g.set_sorter("produces_eggs", SimpleSorter("produces_eggs"))
|
g.set_searchable("name")
|
||||||
g.set_filter("produces_eggs", NullableBooleanFilter)
|
|
||||||
|
|
||||||
# animal_type_name
|
|
||||||
if self.farmos_style_grid_links:
|
|
||||||
g.set_renderer("animal_type_name", self.render_animal_type_for_grid)
|
|
||||||
else:
|
|
||||||
g.set_link("animal_type_name")
|
|
||||||
g.set_sorter("animal_type_name", SimpleSorter("animal_type.name"))
|
|
||||||
g.set_filter("animal_type_name", StringFilter, path="animal_type.name")
|
|
||||||
|
|
||||||
# birthdate
|
# birthdate
|
||||||
g.set_renderer("birthdate", "date")
|
g.set_renderer("birthdate", "date")
|
||||||
g.set_sorter("birthdate", SimpleSorter("birthdate"))
|
|
||||||
g.set_filter("birthdate", DateTimeFilter)
|
|
||||||
|
|
||||||
# sex
|
|
||||||
g.set_enum("sex", enum.ANIMAL_SEX)
|
|
||||||
g.set_sorter("sex", SimpleSorter("sex"))
|
|
||||||
g.set_filter("sex", StringFilter)
|
|
||||||
|
|
||||||
# groups
|
|
||||||
g.set_label("groups", "Group Membership")
|
|
||||||
g.set_renderer("groups", self.render_groups_for_grid)
|
|
||||||
|
|
||||||
# is_sterile
|
# is_sterile
|
||||||
g.set_renderer("is_sterile", "boolean")
|
g.set_renderer("is_sterile", "boolean")
|
||||||
g.set_sorter("is_sterile", SimpleSorter("is_sterile"))
|
|
||||||
g.set_filter("is_sterile", BooleanFilter)
|
|
||||||
|
|
||||||
def render_animal_type_for_grid(self, animal, field, value):
|
# archived
|
||||||
uuid = animal["animal_type"]["uuid"]
|
g.set_renderer("archived", "boolean")
|
||||||
url = self.request.route_url("farmos_animal_types.view", uuid=uuid)
|
|
||||||
return tags.link_to(value, url)
|
|
||||||
|
|
||||||
def render_groups_for_grid(self, animal, field, value):
|
|
||||||
groups = []
|
|
||||||
for group in animal["group_objects"]:
|
|
||||||
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()
|
animal = self.farmos_client.resource.get_id(
|
||||||
|
"asset", "animal", self.request.matchdict["uuid"]
|
||||||
|
)
|
||||||
|
self.raw_json = animal
|
||||||
|
|
||||||
if relationships := self.raw_json["data"].get("relationships"):
|
# instance data
|
||||||
|
data = self.normalize_animal(animal["data"])
|
||||||
|
|
||||||
|
if relationships := animal["data"].get("relationships"):
|
||||||
|
|
||||||
# add animal type
|
# add animal type
|
||||||
if not data.get("animal_type"):
|
|
||||||
if animal_type := relationships.get("animal_type"):
|
if animal_type := relationships.get("animal_type"):
|
||||||
if animal_type["data"]:
|
if animal_type["data"]:
|
||||||
animal_type = self.farmos_client.resource.get_id(
|
animal_type = self.farmos_client.resource.get_id(
|
||||||
|
|
@ -175,10 +124,54 @@ class AnimalView(AssetMasterView):
|
||||||
"name": animal_type["data"]["attributes"]["name"],
|
"name": animal_type["data"]["attributes"]["name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# add location
|
||||||
|
if location := relationships.get("location"):
|
||||||
|
if location["data"]:
|
||||||
|
location = self.farmos_client.resource.get_id(
|
||||||
|
"asset", "structure", location["data"][0]["id"]
|
||||||
|
)
|
||||||
|
data["location"] = {
|
||||||
|
"uuid": location["data"]["id"],
|
||||||
|
"name": location["data"]["attributes"]["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# add owners
|
||||||
|
if owner := relationships.get("owner"):
|
||||||
|
data["owners"] = []
|
||||||
|
for owner_data in owner["data"]:
|
||||||
|
owner = self.farmos_client.resource.get_id(
|
||||||
|
"user", "user", owner_data["id"]
|
||||||
|
)
|
||||||
|
data["owners"].append(
|
||||||
|
{
|
||||||
|
"uuid": owner["data"]["id"],
|
||||||
|
"display_name": owner["data"]["attributes"]["display_name"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# add image urls
|
||||||
|
if image := relationships.get("image"):
|
||||||
|
if image["data"]:
|
||||||
|
image = self.farmos_client.resource.get_id(
|
||||||
|
"file", "file", image["data"][0]["id"]
|
||||||
|
)
|
||||||
|
data["raw_image_url"] = self.app.get_farmos_url(
|
||||||
|
image["data"]["attributes"]["uri"]["url"]
|
||||||
|
)
|
||||||
|
# nb. other styles available: medium, wide
|
||||||
|
data["large_image_url"] = image["data"]["attributes"][
|
||||||
|
"image_style_uri"
|
||||||
|
]["large"]
|
||||||
|
data["thumbnail_image_url"] = image["data"]["attributes"][
|
||||||
|
"image_style_uri"
|
||||||
|
]["thumbnail"]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def normalize_asset(self, animal, included):
|
def get_instance_title(self, animal):
|
||||||
normal = super().normalize_asset(animal, included)
|
return animal["name"]
|
||||||
|
|
||||||
|
def normalize_animal(self, animal):
|
||||||
|
|
||||||
birthdate = animal["attributes"]["birthdate"]
|
birthdate = animal["attributes"]["birthdate"]
|
||||||
if birthdate:
|
if birthdate:
|
||||||
|
|
@ -191,123 +184,72 @@ class AnimalView(AssetMasterView):
|
||||||
else:
|
else:
|
||||||
sterile = animal["attributes"]["is_castrated"]
|
sterile = animal["attributes"]["is_castrated"]
|
||||||
|
|
||||||
animal_type_object = None
|
if notes := animal["attributes"]["notes"]:
|
||||||
group_objects = []
|
notes = notes["value"]
|
||||||
group_names = []
|
|
||||||
if relationships := animal.get("relationships"):
|
|
||||||
|
|
||||||
if animal_type := relationships.get("animal_type"):
|
if self.farmos_4x:
|
||||||
if animal_type := included.get(animal_type["data"]["id"]):
|
archived = animal["attributes"]["archived"]
|
||||||
animal_type_object = {
|
else:
|
||||||
"uuid": animal_type["id"],
|
archived = animal["attributes"]["status"] == "archived"
|
||||||
"name": animal_type["attributes"]["name"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if groups := relationships.get("group"):
|
return {
|
||||||
for group in groups["data"]:
|
"uuid": animal["id"],
|
||||||
if group := included.get(group["id"]):
|
"drupal_id": animal["attributes"]["drupal_internal__id"],
|
||||||
group = {
|
"name": animal["attributes"]["name"],
|
||||||
"uuid": group["id"],
|
|
||||||
"name": group["attributes"]["name"],
|
|
||||||
}
|
|
||||||
group_objects.append(group)
|
|
||||||
group_names.append(group["name"])
|
|
||||||
|
|
||||||
normal.update(
|
|
||||||
{
|
|
||||||
"animal_type": animal_type_object,
|
|
||||||
"animal_type_uuid": animal_type_object["uuid"],
|
|
||||||
"animal_type_name": animal_type_object["name"],
|
|
||||||
"group_objects": 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,
|
||||||
"produces_eggs": animal["attributes"].get("produces_eggs"),
|
"location": colander.null, # TODO
|
||||||
|
"archived": archived,
|
||||||
|
"notes": notes or colander.null,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return normal
|
|
||||||
|
|
||||||
def get_animal_types(self):
|
|
||||||
animal_types = []
|
|
||||||
result = self.farmos_client.resource.get(
|
|
||||||
"taxonomy_term", "animal_type", params={"sort": "name"}
|
|
||||||
)
|
|
||||||
for animal_type in result["data"]:
|
|
||||||
animal_types.append((animal_type["id"], animal_type["attributes"]["name"]))
|
|
||||||
return animal_types
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
f = form
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
enum = self.app.enum
|
|
||||||
animal = f.model_instance
|
animal = f.model_instance
|
||||||
|
|
||||||
# animal_type
|
# animal_type
|
||||||
f.set_node(
|
f.set_node("animal_type", AnimalTypeType(self.request))
|
||||||
"animal_type",
|
|
||||||
FarmOSRef(
|
|
||||||
self.request, "farmos_animal_types", values=self.get_animal_types
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# produces_eggs
|
|
||||||
f.set_node("produces_eggs", colander.Boolean())
|
|
||||||
|
|
||||||
# birthdate
|
# birthdate
|
||||||
f.set_node("birthdate", WuttaDateTime())
|
f.set_node("birthdate", WuttaDateTime())
|
||||||
f.set_widget("birthdate", WuttaDateTimeWidget(self.request))
|
f.set_widget("birthdate", WuttaDateTimeWidget(self.request))
|
||||||
f.set_required("birthdate", False)
|
|
||||||
|
|
||||||
# sex
|
|
||||||
if not (self.creating or self.editing) and not animal["sex"]:
|
|
||||||
pass # TODO: dict enum widget does not handle null values well
|
|
||||||
else:
|
|
||||||
f.set_node("sex", WuttaDictEnum(self.request, enum.ANIMAL_SEX))
|
|
||||||
f.set_required("sex", False)
|
|
||||||
|
|
||||||
# is_sterile
|
# is_sterile
|
||||||
f.set_node("is_sterile", colander.Boolean())
|
f.set_node("is_sterile", colander.Boolean())
|
||||||
|
|
||||||
# groups
|
# location
|
||||||
if self.creating or self.editing:
|
f.set_node("location", StructureType(self.request))
|
||||||
f.remove("groups") # TODO
|
|
||||||
|
|
||||||
def get_api_payload(self, animal):
|
# owners
|
||||||
payload = super().get_api_payload(animal)
|
f.set_node("owners", UsersType(self.request))
|
||||||
|
|
||||||
birthdate = None
|
# notes
|
||||||
if animal["birthdate"]:
|
f.set_widget("notes", "notes")
|
||||||
birthdate = self.app.localtime(animal["birthdate"]).timestamp()
|
|
||||||
|
|
||||||
attrs = {
|
# archived
|
||||||
"sex": animal["sex"] or None,
|
f.set_node("archived", colander.Boolean())
|
||||||
"is_sterile": animal["is_sterile"],
|
|
||||||
"produces_eggs": animal["produces_eggs"],
|
|
||||||
"birthdate": birthdate,
|
|
||||||
}
|
|
||||||
|
|
||||||
rels = {
|
# image
|
||||||
"animal_type": {
|
if url := animal.get("large_image_url"):
|
||||||
"data": {
|
f.set_widget("image", ImageWidget("animal image"))
|
||||||
"id": animal["animal_type"],
|
f.set_default("image", url)
|
||||||
"type": "taxonomy_term--animal_type",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload["attributes"].update(attrs)
|
|
||||||
payload.setdefault("relationships", {}).update(rels)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
def get_xref_buttons(self, animal):
|
def get_xref_buttons(self, animal):
|
||||||
buttons = super().get_xref_buttons(animal)
|
|
||||||
|
|
||||||
if self.app.is_farmos_mirror():
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
self.make_button(
|
||||||
|
"View in farmOS",
|
||||||
|
primary=True,
|
||||||
|
url=self.app.get_farmos_url(f"/asset/{animal['drupal_id']}"),
|
||||||
|
target="_blank",
|
||||||
|
icon_left="external-link-alt",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
if wf_animal := (
|
if wf_animal := (
|
||||||
session.query(model.Asset)
|
session.query(model.Asset)
|
||||||
.filter(model.Asset.farmos_uuid == animal["uuid"])
|
.filter(model.Asset.farmos_uuid == animal["uuid"])
|
||||||
|
|
|
||||||
|
|
@ -1,317 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Base class for Asset master views
|
|
||||||
"""
|
|
||||||
|
|
||||||
import colander
|
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
|
||||||
from wuttafarm.web.forms.schema import FarmOSRefs, FarmOSLocationRefs
|
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
|
||||||
from wuttafarm.web.grids import (
|
|
||||||
ResourceData,
|
|
||||||
StringFilter,
|
|
||||||
IntegerFilter,
|
|
||||||
BooleanFilter,
|
|
||||||
SimpleSorter,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AssetMasterView(FarmOSMasterView):
|
|
||||||
"""
|
|
||||||
Base class for Asset master views
|
|
||||||
"""
|
|
||||||
|
|
||||||
farmos_asset_type = None
|
|
||||||
creatable = True
|
|
||||||
editable = True
|
|
||||||
deletable = True
|
|
||||||
filterable = True
|
|
||||||
sort_on_backend = True
|
|
||||||
|
|
||||||
labels = {
|
|
||||||
"name": "Asset Name",
|
|
||||||
"asset_type_name": "Asset Type",
|
|
||||||
"owners": "Owner",
|
|
||||||
"locations": "Location",
|
|
||||||
"thumbnail_url": "Thumbnail URL",
|
|
||||||
"image_url": "Image URL",
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"thumbnail",
|
|
||||||
"drupal_id",
|
|
||||||
"name",
|
|
||||||
"owners",
|
|
||||||
"locations",
|
|
||||||
"archived",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = "name"
|
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
"name": {"active": True, "verb": "contains"},
|
|
||||||
"archived": {"active": True, "verb": "is_false"},
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_grid_data(self, **kwargs):
|
|
||||||
return ResourceData(
|
|
||||||
self.config,
|
|
||||||
self.farmos_client,
|
|
||||||
f"asset--{self.farmos_asset_type}",
|
|
||||||
include=",".join(self.get_farmos_api_includes()),
|
|
||||||
normalizer=self.normalize_asset,
|
|
||||||
)
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# thumbnail
|
|
||||||
g.set_renderer("thumbnail", self.render_grid_thumbnail)
|
|
||||||
g.set_label("thumbnail", "", column_only=True)
|
|
||||||
g.set_centered("thumbnail")
|
|
||||||
|
|
||||||
# drupal_id
|
|
||||||
g.set_label("drupal_id", "ID", column_only=True)
|
|
||||||
g.set_sorter("drupal_id", SimpleSorter("drupal_internal__id"))
|
|
||||||
g.set_filter("drupal_id", IntegerFilter, path="drupal_internal__id")
|
|
||||||
|
|
||||||
# name
|
|
||||||
g.set_link("name")
|
|
||||||
g.set_sorter("name", SimpleSorter("name"))
|
|
||||||
g.set_filter("name", StringFilter)
|
|
||||||
|
|
||||||
# owners
|
|
||||||
g.set_renderer("owners", self.render_owners_for_grid)
|
|
||||||
|
|
||||||
# locations
|
|
||||||
g.set_renderer("locations", self.render_locations_for_grid)
|
|
||||||
|
|
||||||
# archived
|
|
||||||
g.set_renderer("archived", "boolean")
|
|
||||||
g.set_sorter("archived", SimpleSorter("archived"))
|
|
||||||
g.set_filter("archived", BooleanFilter)
|
|
||||||
|
|
||||||
def render_grid_thumbnail(self, obj, field, value):
|
|
||||||
if url := obj.get("thumbnail_url"):
|
|
||||||
return tags.image(url, f"thumbnail for {self.get_model_title()}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_locations_for_grid(self, asset, field, value):
|
|
||||||
locations = []
|
|
||||||
for location in value:
|
|
||||||
if self.farmos_style_grid_links:
|
|
||||||
asset_type = location["type"].split("--")[1]
|
|
||||||
route = f"farmos_{asset_type}_assets.view"
|
|
||||||
url = self.request.route_url(route, uuid=location["uuid"])
|
|
||||||
locations.append(tags.link_to(location["name"], url))
|
|
||||||
else:
|
|
||||||
locations.append(location["name"])
|
|
||||||
return ", ".join(locations)
|
|
||||||
|
|
||||||
def grid_row_class(self, asset, data, i):
|
|
||||||
""" """
|
|
||||||
if asset["archived"]:
|
|
||||||
return "has-background-warning"
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_farmos_api_includes(self):
|
|
||||||
return {"asset_type", "location", "owner", "image"}
|
|
||||||
|
|
||||||
def get_instance(self):
|
|
||||||
result = self.farmos_client.asset.get_id(
|
|
||||||
self.farmos_asset_type,
|
|
||||||
self.request.matchdict["uuid"],
|
|
||||||
params={"include": ",".join(self.get_farmos_api_includes())},
|
|
||||||
)
|
|
||||||
self.raw_json = result
|
|
||||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
|
||||||
return self.normalize_asset(result["data"], included)
|
|
||||||
|
|
||||||
def get_instance_title(self, asset):
|
|
||||||
return asset["name"]
|
|
||||||
|
|
||||||
def normalize_asset(self, asset, included):
|
|
||||||
|
|
||||||
if notes := asset["attributes"]["notes"]:
|
|
||||||
notes = notes["value"]
|
|
||||||
|
|
||||||
if self.farmos_4x:
|
|
||||||
archived = asset["attributes"]["archived"]
|
|
||||||
else:
|
|
||||||
archived = asset["attributes"]["status"] == "archived"
|
|
||||||
|
|
||||||
asset_type_object = {}
|
|
||||||
asset_type_name = None
|
|
||||||
owner_objects = []
|
|
||||||
owner_names = []
|
|
||||||
location_objects = []
|
|
||||||
location_names = []
|
|
||||||
thumbnail_url = None
|
|
||||||
image_url = None
|
|
||||||
if relationships := asset.get("relationships"):
|
|
||||||
|
|
||||||
if asset_type := relationships.get("asset_type"):
|
|
||||||
if asset_type := included.get(asset_type["data"]["id"]):
|
|
||||||
asset_type_object = {
|
|
||||||
"uuid": asset_type["id"],
|
|
||||||
"name": asset_type["attributes"]["label"],
|
|
||||||
}
|
|
||||||
asset_type_name = asset_type_object["name"]
|
|
||||||
|
|
||||||
if owners := relationships.get("owner"):
|
|
||||||
for user in owners["data"]:
|
|
||||||
if user := included.get(user["id"]):
|
|
||||||
user = {
|
|
||||||
"uuid": user["id"],
|
|
||||||
"name": user["attributes"]["name"],
|
|
||||||
}
|
|
||||||
owner_objects.append(user)
|
|
||||||
owner_names.append(user["name"])
|
|
||||||
|
|
||||||
if locations := relationships.get("location"):
|
|
||||||
for location in locations["data"]:
|
|
||||||
if location := included.get(location["id"]):
|
|
||||||
location = {
|
|
||||||
"uuid": location["id"],
|
|
||||||
"type": location["type"],
|
|
||||||
"name": location["attributes"]["name"],
|
|
||||||
}
|
|
||||||
location_objects.append(location)
|
|
||||||
location_names.append(location["name"])
|
|
||||||
|
|
||||||
if images := relationships.get("image"):
|
|
||||||
for image in images["data"]:
|
|
||||||
if image := included.get(image["id"]):
|
|
||||||
thumbnail_url = image["attributes"]["image_style_uri"][
|
|
||||||
"thumbnail"
|
|
||||||
]
|
|
||||||
image_url = image["attributes"]["image_style_uri"]["large"]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"uuid": asset["id"],
|
|
||||||
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
|
||||||
"name": asset["attributes"]["name"],
|
|
||||||
"asset_type": asset_type_object,
|
|
||||||
"asset_type_name": asset_type_name,
|
|
||||||
"notes": notes or colander.null,
|
|
||||||
"owners": owner_objects,
|
|
||||||
"owner_names": owner_names,
|
|
||||||
"locations": location_objects,
|
|
||||||
"location_names": location_names,
|
|
||||||
"archived": archived,
|
|
||||||
"thumbnail_url": thumbnail_url or colander.null,
|
|
||||||
"image_url": image_url or colander.null,
|
|
||||||
}
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
|
||||||
asset = f.model_instance
|
|
||||||
|
|
||||||
# asset_type_name
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("asset_type_name")
|
|
||||||
|
|
||||||
# locations
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("locations")
|
|
||||||
else:
|
|
||||||
f.set_node("locations", FarmOSLocationRefs(self.request))
|
|
||||||
|
|
||||||
# owners
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("owners") # TODO
|
|
||||||
else:
|
|
||||||
f.set_node("owners", FarmOSRefs(self.request, "farmos_users"))
|
|
||||||
|
|
||||||
# notes
|
|
||||||
f.set_widget("notes", "notes")
|
|
||||||
f.set_required("notes", False)
|
|
||||||
|
|
||||||
# archived
|
|
||||||
f.set_node("archived", colander.Boolean())
|
|
||||||
|
|
||||||
# thumbnail_url
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("thumbnail_url")
|
|
||||||
|
|
||||||
# image_url
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("image_url")
|
|
||||||
|
|
||||||
# thumbnail
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("thumbnail")
|
|
||||||
elif asset.get("thumbnail_url"):
|
|
||||||
f.set_widget("thumbnail", ImageWidget("asset thumbnail"))
|
|
||||||
f.set_default("thumbnail", asset["thumbnail_url"])
|
|
||||||
|
|
||||||
# image
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("image")
|
|
||||||
elif asset.get("image_url"):
|
|
||||||
f.set_widget("image", ImageWidget("asset image"))
|
|
||||||
f.set_default("image", asset["image_url"])
|
|
||||||
|
|
||||||
def persist(self, asset, session=None):
|
|
||||||
payload = self.get_api_payload(asset)
|
|
||||||
if self.editing:
|
|
||||||
payload["id"] = asset["uuid"]
|
|
||||||
|
|
||||||
result = self.farmos_client.asset.send(self.farmos_asset_type, payload)
|
|
||||||
|
|
||||||
if self.creating:
|
|
||||||
asset["uuid"] = result["data"]["id"]
|
|
||||||
|
|
||||||
def get_api_payload(self, asset):
|
|
||||||
|
|
||||||
attrs = {
|
|
||||||
"name": asset["name"],
|
|
||||||
"notes": {"value": asset["notes"] or None},
|
|
||||||
"archived": asset["archived"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if "is_location" in asset:
|
|
||||||
attrs["is_location"] = asset["is_location"]
|
|
||||||
|
|
||||||
if "is_fixed" in asset:
|
|
||||||
attrs["is_fixed"] = asset["is_fixed"]
|
|
||||||
|
|
||||||
return {"attributes": attrs}
|
|
||||||
|
|
||||||
def delete_instance(self, asset):
|
|
||||||
self.farmos_client.asset.delete(self.farmos_asset_type, asset["uuid"])
|
|
||||||
|
|
||||||
def get_xref_buttons(self, asset):
|
|
||||||
return [
|
|
||||||
self.make_button(
|
|
||||||
"View in farmOS",
|
|
||||||
primary=True,
|
|
||||||
url=self.app.get_farmos_url(f"/asset/{asset['drupal_id']}"),
|
|
||||||
target="_blank",
|
|
||||||
icon_left="external-link-alt",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -41,7 +41,7 @@ class GroupView(FarmOSMasterView):
|
||||||
model_title = "farmOS Group"
|
model_title = "farmOS Group"
|
||||||
model_title_plural = "farmOS Groups"
|
model_title_plural = "farmOS Groups"
|
||||||
|
|
||||||
route_prefix = "farmos_group_assets"
|
route_prefix = "farmos_groups"
|
||||||
url_prefix = "/farmOS/groups"
|
url_prefix = "/farmOS/groups"
|
||||||
|
|
||||||
farmos_refurl_path = "/assets/group"
|
farmos_refurl_path = "/assets/group"
|
||||||
|
|
|
||||||
|
|
@ -23,27 +23,14 @@
|
||||||
View for farmOS Harvest Logs
|
View for farmOS Harvest Logs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
import datetime
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum, Notes
|
import colander
|
||||||
|
|
||||||
|
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.grids import (
|
|
||||||
ResourceData,
|
|
||||||
SimpleSorter,
|
|
||||||
StringFilter,
|
|
||||||
IntegerFilter,
|
|
||||||
DateTimeFilter,
|
|
||||||
NullableBooleanFilter,
|
|
||||||
)
|
|
||||||
from wuttafarm.web.forms.schema import (
|
|
||||||
FarmOSQuantityRefs,
|
|
||||||
FarmOSAssetRefs,
|
|
||||||
FarmOSRefs,
|
|
||||||
LogQuick,
|
|
||||||
)
|
|
||||||
from wuttafarm.web.util import render_quantity_objects
|
|
||||||
|
|
||||||
|
|
||||||
class LogMasterView(FarmOSMasterView):
|
class LogMasterView(FarmOSMasterView):
|
||||||
|
|
@ -52,180 +39,75 @@ class LogMasterView(FarmOSMasterView):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
farmos_log_type = None
|
farmos_log_type = None
|
||||||
filterable = True
|
|
||||||
sort_on_backend = True
|
|
||||||
|
|
||||||
labels = {
|
|
||||||
"name": "Log Name",
|
|
||||||
"log_type_name": "Log Type",
|
|
||||||
"quantities": "Quantity",
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"status",
|
|
||||||
"drupal_id",
|
|
||||||
"timestamp",
|
|
||||||
"name",
|
"name",
|
||||||
"assets",
|
"timestamp",
|
||||||
"quantities",
|
"status",
|
||||||
"is_group_assignment",
|
|
||||||
"owners",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = ("timestamp", "desc")
|
sort_defaults = ("timestamp", "desc")
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
"name": {"active": True, "verb": "contains"},
|
|
||||||
"status": {"active": True, "verb": "not_equal", "value": "abandoned"},
|
|
||||||
}
|
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"assets",
|
|
||||||
"quantities",
|
|
||||||
"notes",
|
|
||||||
"status",
|
"status",
|
||||||
"log_type_name",
|
"notes",
|
||||||
"owners",
|
|
||||||
"quick",
|
|
||||||
"drupal_id",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_farmos_api_includes(self):
|
def get_grid_data(self, columns=None, session=None):
|
||||||
return {"log_type", "quantity", "asset", "owner"}
|
result = self.farmos_client.log.get(self.farmos_log_type)
|
||||||
|
return [self.normalize_log(l) for l in result["data"]]
|
||||||
def get_grid_data(self, **kwargs):
|
|
||||||
return ResourceData(
|
|
||||||
self.config,
|
|
||||||
self.farmos_client,
|
|
||||||
f"log--{self.farmos_log_type}",
|
|
||||||
include=",".join(self.get_farmos_api_includes()),
|
|
||||||
normalizer=self.normalize_log,
|
|
||||||
)
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
enum = self.app.enum
|
|
||||||
|
|
||||||
# status
|
|
||||||
g.set_enum("status", enum.LOG_STATUS)
|
|
||||||
g.set_sorter("status", SimpleSorter("status"))
|
|
||||||
g.set_filter(
|
|
||||||
"status",
|
|
||||||
StringFilter,
|
|
||||||
choices=enum.LOG_STATUS,
|
|
||||||
verbs=["equal", "not_equal"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# drupal_id
|
|
||||||
g.set_label("drupal_id", "ID", column_only=True)
|
|
||||||
g.set_sorter("drupal_id", SimpleSorter("drupal_internal__id"))
|
|
||||||
g.set_filter("drupal_id", IntegerFilter, path="drupal_internal__id")
|
|
||||||
|
|
||||||
# timestamp
|
|
||||||
g.set_renderer("timestamp", "date")
|
|
||||||
g.set_link("timestamp")
|
|
||||||
g.set_sorter("timestamp", SimpleSorter("timestamp"))
|
|
||||||
g.set_filter("timestamp", DateTimeFilter)
|
|
||||||
|
|
||||||
# name
|
# name
|
||||||
g.set_link("name")
|
g.set_link("name")
|
||||||
g.set_sorter("name", SimpleSorter("name"))
|
g.set_searchable("name")
|
||||||
g.set_filter("name", StringFilter)
|
|
||||||
|
|
||||||
# assets
|
# timestamp
|
||||||
g.set_renderer("assets", self.render_assets_for_grid)
|
g.set_renderer("timestamp", "datetime")
|
||||||
|
|
||||||
# quantities
|
|
||||||
g.set_renderer("quantities", self.render_quantities_for_grid)
|
|
||||||
|
|
||||||
# is_group_assignment
|
|
||||||
g.set_renderer("is_group_assignment", "boolean")
|
|
||||||
g.set_sorter("is_group_assignment", SimpleSorter("is_group_assignment"))
|
|
||||||
g.set_filter("is_group_assignment", NullableBooleanFilter)
|
|
||||||
|
|
||||||
# owners
|
|
||||||
g.set_label("owners", "Owner")
|
|
||||||
g.set_renderer("owners", self.render_owners_for_grid)
|
|
||||||
|
|
||||||
def render_assets_for_grid(self, log, field, value):
|
|
||||||
assets = []
|
|
||||||
for asset in value:
|
|
||||||
if self.farmos_style_grid_links:
|
|
||||||
url = self.request.route_url(
|
|
||||||
f"farmos_{asset['asset_type']}_assets.view", uuid=asset["uuid"]
|
|
||||||
)
|
|
||||||
assets.append(tags.link_to(asset["name"], url))
|
|
||||||
else:
|
|
||||||
assets.append(asset["name"])
|
|
||||||
return ", ".join(assets)
|
|
||||||
|
|
||||||
def render_quantities_for_grid(self, log, field, value):
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
return render_quantity_objects(value)
|
|
||||||
|
|
||||||
def grid_row_class(self, log, data, i):
|
|
||||||
if log["status"] == "pending":
|
|
||||||
return "has-background-warning"
|
|
||||||
if log["status"] == "abandoned":
|
|
||||||
return "has-background-danger"
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
result = self.farmos_client.log.get_id(
|
log = self.farmos_client.log.get_id(
|
||||||
self.farmos_log_type,
|
self.farmos_log_type, self.request.matchdict["uuid"]
|
||||||
self.request.matchdict["uuid"],
|
|
||||||
params={"include": ",".join(self.get_farmos_api_includes())},
|
|
||||||
)
|
)
|
||||||
self.raw_json = result
|
self.raw_json = log
|
||||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
return self.normalize_log(log["data"])
|
||||||
return self.normalize_log(result["data"], included)
|
|
||||||
|
|
||||||
def get_instance_title(self, log):
|
def get_instance_title(self, log):
|
||||||
return log["name"]
|
return log["name"]
|
||||||
|
|
||||||
def normalize_log(self, log, included):
|
def normalize_log(self, log):
|
||||||
data = self.normal.normalize_farmos_log(log, included)
|
|
||||||
data.update(
|
if timestamp := log["attributes"]["timestamp"]:
|
||||||
{
|
timestamp = datetime.datetime.fromisoformat(timestamp)
|
||||||
"log_type_name": data["log_type"].get("name"),
|
timestamp = self.app.localtime(timestamp)
|
||||||
|
|
||||||
|
if notes := log["attributes"]["notes"]:
|
||||||
|
notes = notes["value"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uuid": log["id"],
|
||||||
|
"drupal_id": log["attributes"]["drupal_internal__id"],
|
||||||
|
"name": log["attributes"]["name"],
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"status": log["attributes"]["status"],
|
||||||
|
"notes": notes or colander.null,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
f = form
|
f = form
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
enum = self.app.enum
|
|
||||||
log = f.model_instance
|
|
||||||
|
|
||||||
# timestamp
|
# timestamp
|
||||||
f.set_node("timestamp", WuttaDateTime())
|
f.set_node("timestamp", WuttaDateTime())
|
||||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
||||||
|
|
||||||
# assets
|
|
||||||
f.set_node("assets", FarmOSAssetRefs(self.request))
|
|
||||||
|
|
||||||
# quantities
|
|
||||||
f.set_node("quantities", FarmOSQuantityRefs(self.request))
|
|
||||||
|
|
||||||
# notes
|
# notes
|
||||||
f.set_node("notes", Notes())
|
f.set_widget("notes", "notes")
|
||||||
|
|
||||||
# status
|
|
||||||
f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS))
|
|
||||||
|
|
||||||
# owners
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("owners") # TODO
|
|
||||||
else:
|
|
||||||
f.set_node("owners", FarmOSRefs(self.request, "farmos_users"))
|
|
||||||
|
|
||||||
# quick
|
|
||||||
f.set_node("quick", LogQuick(self.request))
|
|
||||||
|
|
||||||
def get_xref_buttons(self, log):
|
def get_xref_buttons(self, log):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,6 @@ class HarvestLogView(LogMasterView):
|
||||||
farmos_log_type = "harvest"
|
farmos_log_type = "harvest"
|
||||||
farmos_refurl_path = "/logs/harvest"
|
farmos_refurl_path = "/logs/harvest"
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"status",
|
|
||||||
"drupal_id",
|
|
||||||
"timestamp",
|
|
||||||
"name",
|
|
||||||
"assets",
|
|
||||||
"quantities",
|
|
||||||
"is_group_assignment",
|
|
||||||
"owners",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,13 @@
|
||||||
Base class for farmOS master views
|
Base class for farmOS master views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import colander
|
|
||||||
import markdown
|
import markdown
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.forms.schema import WuttaDateTime
|
|
||||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
|
||||||
|
|
||||||
from wuttafarm.web.util import save_farmos_oauth2_token, use_farmos_style_grid_links
|
from wuttafarm.web.util import save_farmos_oauth2_token
|
||||||
from wuttafarm.web.grids import (
|
|
||||||
ResourceData,
|
|
||||||
StringFilter,
|
|
||||||
NullableStringFilter,
|
|
||||||
DateTimeFilter,
|
|
||||||
SimpleSorter,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FarmOSMasterView(MasterView):
|
class FarmOSMasterView(MasterView):
|
||||||
|
|
@ -62,7 +50,6 @@ class FarmOSMasterView(MasterView):
|
||||||
farmos_refurl_path = None
|
farmos_refurl_path = None
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"drupal_id": "Drupal ID",
|
|
||||||
"raw_image_url": "Raw Image URL",
|
"raw_image_url": "Raw Image URL",
|
||||||
"large_image_url": "Large Image URL",
|
"large_image_url": "Large Image URL",
|
||||||
"thumbnail_image_url": "Thumbnail Image URL",
|
"thumbnail_image_url": "Thumbnail Image URL",
|
||||||
|
|
@ -72,9 +59,7 @@ class FarmOSMasterView(MasterView):
|
||||||
super().__init__(request, context=context)
|
super().__init__(request, context=context)
|
||||||
self.farmos_client = self.get_farmos_client()
|
self.farmos_client = self.get_farmos_client()
|
||||||
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
||||||
self.normal = self.app.get_normalizer(self.farmos_client)
|
|
||||||
self.raw_json = None
|
self.raw_json = None
|
||||||
self.farmos_style_grid_links = use_farmos_style_grid_links(self.config)
|
|
||||||
|
|
||||||
def get_farmos_client(self):
|
def get_farmos_client(self):
|
||||||
token = self.request.session.get("farmos.oauth2.token")
|
token = self.request.session.get("farmos.oauth2.token")
|
||||||
|
|
@ -101,16 +86,6 @@ class FarmOSMasterView(MasterView):
|
||||||
|
|
||||||
return templates
|
return templates
|
||||||
|
|
||||||
def render_owners_for_grid(self, obj, field, value):
|
|
||||||
owners = []
|
|
||||||
for user in value:
|
|
||||||
if self.farmos_style_grid_links:
|
|
||||||
url = self.request.route_url("farmos_users.view", uuid=user["uuid"])
|
|
||||||
owners.append(tags.link_to(user["name"], url))
|
|
||||||
else:
|
|
||||||
owners.append(user["name"])
|
|
||||||
return ", ".join(owners)
|
|
||||||
|
|
||||||
def get_template_context(self, context):
|
def get_template_context(self, context):
|
||||||
|
|
||||||
if self.listing and self.farmos_refurl_path:
|
if self.listing and self.farmos_refurl_path:
|
||||||
|
|
@ -125,143 +100,3 @@ class FarmOSMasterView(MasterView):
|
||||||
)
|
)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TaxonomyMasterView(FarmOSMasterView):
|
|
||||||
"""
|
|
||||||
Base class for farmOS "taxonomy term" views
|
|
||||||
"""
|
|
||||||
|
|
||||||
farmos_taxonomy_type = None
|
|
||||||
creatable = True
|
|
||||||
editable = True
|
|
||||||
deletable = True
|
|
||||||
filterable = True
|
|
||||||
sort_on_backend = True
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"changed",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = "name"
|
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
"name": {"active": True, "verb": "contains"},
|
|
||||||
}
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"changed",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_grid_data(self, columns=None, session=None):
|
|
||||||
return ResourceData(
|
|
||||||
self.config,
|
|
||||||
self.farmos_client,
|
|
||||||
f"taxonomy_term--{self.farmos_taxonomy_type}",
|
|
||||||
normalizer=self.normalize_taxonomy_term,
|
|
||||||
)
|
|
||||||
|
|
||||||
def normalize_taxonomy_term(self, term, included):
|
|
||||||
|
|
||||||
if changed := term["attributes"]["changed"]:
|
|
||||||
changed = datetime.datetime.fromisoformat(changed)
|
|
||||||
changed = self.app.localtime(changed)
|
|
||||||
|
|
||||||
if description := term["attributes"]["description"]:
|
|
||||||
description = description["value"]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"uuid": term["id"],
|
|
||||||
"drupal_id": term["attributes"]["drupal_internal__tid"],
|
|
||||||
"name": term["attributes"]["name"],
|
|
||||||
"description": description or colander.null,
|
|
||||||
"changed": changed,
|
|
||||||
}
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# name
|
|
||||||
g.set_link("name")
|
|
||||||
g.set_sorter("name", SimpleSorter("name"))
|
|
||||||
g.set_filter("name", StringFilter)
|
|
||||||
|
|
||||||
# description
|
|
||||||
g.set_sorter("description", SimpleSorter("description.value"))
|
|
||||||
g.set_filter("description", NullableStringFilter, path="description.value")
|
|
||||||
|
|
||||||
# changed
|
|
||||||
g.set_renderer("changed", "datetime")
|
|
||||||
g.set_sorter("changed", SimpleSorter("changed"))
|
|
||||||
g.set_filter("changed", DateTimeFilter)
|
|
||||||
|
|
||||||
def get_instance(self):
|
|
||||||
result = self.farmos_client.resource.get_id(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type, self.request.matchdict["uuid"]
|
|
||||||
)
|
|
||||||
self.raw_json = result
|
|
||||||
return self.normalize_taxonomy_term(result["data"], {})
|
|
||||||
|
|
||||||
def get_instance_title(self, term):
|
|
||||||
return term["name"]
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
|
||||||
|
|
||||||
# description
|
|
||||||
f.set_widget("description", "notes")
|
|
||||||
f.set_required("description", False)
|
|
||||||
|
|
||||||
# changed
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("changed")
|
|
||||||
else:
|
|
||||||
f.set_node("changed", WuttaDateTime())
|
|
||||||
f.set_widget("changed", WuttaDateTimeWidget(self.request))
|
|
||||||
|
|
||||||
def get_api_payload(self, term):
|
|
||||||
|
|
||||||
attrs = {
|
|
||||||
"name": term["name"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if description := term["description"]:
|
|
||||||
attrs["description"] = {"value": description}
|
|
||||||
else:
|
|
||||||
attrs["description"] = None
|
|
||||||
|
|
||||||
return {"attributes": attrs}
|
|
||||||
|
|
||||||
def persist(self, term, session=None):
|
|
||||||
payload = self.get_api_payload(term)
|
|
||||||
if self.editing:
|
|
||||||
payload["id"] = term["uuid"]
|
|
||||||
|
|
||||||
result = self.farmos_client.resource.send(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type, payload
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.creating:
|
|
||||||
term["uuid"] = result["data"]["id"]
|
|
||||||
|
|
||||||
def delete_instance(self, term):
|
|
||||||
self.farmos_client.resource.delete(
|
|
||||||
"taxonomy_term", self.farmos_taxonomy_type, term["uuid"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_xref_buttons(self, term):
|
|
||||||
return [
|
|
||||||
self.make_button(
|
|
||||||
"View in farmOS",
|
|
||||||
primary=True,
|
|
||||||
url=self.app.get_farmos_url(f"/taxonomy/term/{term['drupal_id']}"),
|
|
||||||
target="_blank",
|
|
||||||
icon_left="external-link-alt",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,12 @@ import colander
|
||||||
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.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
|
||||||
from wuttafarm.web.forms.widgets import ImageWidget
|
from wuttafarm.web.forms.widgets import ImageWidget
|
||||||
|
|
||||||
|
|
||||||
class PlantTypeView(TaxonomyMasterView):
|
class PlantTypeView(FarmOSMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Plant Types in farmOS.
|
Master view for Plant Types in farmOS.
|
||||||
"""
|
"""
|
||||||
|
|
@ -48,14 +47,90 @@ class PlantTypeView(TaxonomyMasterView):
|
||||||
route_prefix = "farmos_plant_types"
|
route_prefix = "farmos_plant_types"
|
||||||
url_prefix = "/farmOS/plant-types"
|
url_prefix = "/farmOS/plant-types"
|
||||||
|
|
||||||
farmos_taxonomy_type = "plant_type"
|
|
||||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/plant_type/overview"
|
farmos_refurl_path = "/admin/structure/taxonomy/manage/plant_type/overview"
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"changed",
|
||||||
|
]
|
||||||
|
|
||||||
|
sort_defaults = "name"
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"changed",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_grid_data(self, columns=None, session=None):
|
||||||
|
result = self.farmos_client.resource.get("taxonomy_term", "plant_type")
|
||||||
|
return [self.normalize_plant_type(t) for t in result["data"]]
|
||||||
|
|
||||||
|
def configure_grid(self, grid):
|
||||||
|
g = grid
|
||||||
|
super().configure_grid(g)
|
||||||
|
|
||||||
|
# name
|
||||||
|
g.set_link("name")
|
||||||
|
g.set_searchable("name")
|
||||||
|
|
||||||
|
# changed
|
||||||
|
g.set_renderer("changed", "datetime")
|
||||||
|
|
||||||
|
def get_instance(self):
|
||||||
|
plant_type = self.farmos_client.resource.get_id(
|
||||||
|
"taxonomy_term", "plant_type", self.request.matchdict["uuid"]
|
||||||
|
)
|
||||||
|
self.raw_json = plant_type
|
||||||
|
return self.normalize_plant_type(plant_type["data"])
|
||||||
|
|
||||||
|
def get_instance_title(self, plant_type):
|
||||||
|
return plant_type["name"]
|
||||||
|
|
||||||
|
def normalize_plant_type(self, plant_type):
|
||||||
|
|
||||||
|
if changed := plant_type["attributes"]["changed"]:
|
||||||
|
changed = datetime.datetime.fromisoformat(changed)
|
||||||
|
changed = self.app.localtime(changed)
|
||||||
|
|
||||||
|
if description := plant_type["attributes"]["description"]:
|
||||||
|
description = description["value"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uuid": plant_type["id"],
|
||||||
|
"drupal_id": plant_type["attributes"]["drupal_internal__tid"],
|
||||||
|
"name": plant_type["attributes"]["name"],
|
||||||
|
"description": description or colander.null,
|
||||||
|
"changed": changed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure_form(self, form):
|
||||||
|
f = form
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# description
|
||||||
|
f.set_widget("description", "notes")
|
||||||
|
|
||||||
|
# changed
|
||||||
|
f.set_node("changed", WuttaDateTime())
|
||||||
|
|
||||||
def get_xref_buttons(self, plant_type):
|
def get_xref_buttons(self, plant_type):
|
||||||
buttons = super().get_xref_buttons(plant_type)
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
self.make_button(
|
||||||
|
"View in farmOS",
|
||||||
|
primary=True,
|
||||||
|
url=self.app.get_farmos_url(
|
||||||
|
f"/taxonomy/term/{plant_type['drupal_id']}"
|
||||||
|
),
|
||||||
|
target="_blank",
|
||||||
|
icon_left="external-link-alt",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
if wf_plant_type := (
|
if wf_plant_type := (
|
||||||
session.query(model.PlantType)
|
session.query(model.PlantType)
|
||||||
.filter(model.PlantType.farmos_uuid == plant_type["uuid"])
|
.filter(model.PlantType.farmos_uuid == plant_type["uuid"])
|
||||||
|
|
@ -80,11 +155,11 @@ class PlantAssetView(FarmOSMasterView):
|
||||||
Master view for farmOS Plant Assets
|
Master view for farmOS Plant Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_name = "farmos_plant_assets"
|
model_name = "farmos_asset_plant"
|
||||||
model_title = "farmOS Plant Asset"
|
model_title = "farmOS Plant Asset"
|
||||||
model_title_plural = "farmOS Plant Assets"
|
model_title_plural = "farmOS Plant Assets"
|
||||||
|
|
||||||
route_prefix = "farmos_plant_assets"
|
route_prefix = "farmos_asset_plant"
|
||||||
url_prefix = "/farmOS/assets/plant"
|
url_prefix = "/farmOS/assets/plant"
|
||||||
|
|
||||||
farmos_refurl_path = "/assets/plant"
|
farmos_refurl_path = "/assets/plant"
|
||||||
|
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
View for farmOS Quantity Types
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import colander
|
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDateTime
|
|
||||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
|
||||||
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityTypeView(FarmOSMasterView):
|
|
||||||
"""
|
|
||||||
View for farmOS Quantity Types
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_name = "farmos_quantity_type"
|
|
||||||
model_title = "farmOS Quantity Type"
|
|
||||||
model_title_plural = "farmOS Quantity Types"
|
|
||||||
|
|
||||||
route_prefix = "farmos_quantity_types"
|
|
||||||
url_prefix = "/farmOS/quantity-types"
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"label",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = "label"
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"label",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_grid_data(self, columns=None, session=None):
|
|
||||||
result = self.farmos_client.resource.get("quantity_type")
|
|
||||||
return [self.normalize_quantity_type(t) for t in result["data"]]
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# label
|
|
||||||
g.set_link("label")
|
|
||||||
g.set_searchable("label")
|
|
||||||
|
|
||||||
# description
|
|
||||||
g.set_searchable("description")
|
|
||||||
|
|
||||||
def get_instance(self):
|
|
||||||
result = self.farmos_client.resource.get_id(
|
|
||||||
"quantity_type", "quantity_type", self.request.matchdict["uuid"]
|
|
||||||
)
|
|
||||||
self.raw_json = result
|
|
||||||
return self.normalize_quantity_type(result["data"])
|
|
||||||
|
|
||||||
def get_instance_title(self, quantity_type):
|
|
||||||
return quantity_type["label"]
|
|
||||||
|
|
||||||
def normalize_quantity_type(self, quantity_type):
|
|
||||||
return {
|
|
||||||
"uuid": quantity_type["id"],
|
|
||||||
"drupal_id": quantity_type["attributes"]["drupal_internal__id"],
|
|
||||||
"label": quantity_type["attributes"]["label"],
|
|
||||||
"description": quantity_type["attributes"]["description"],
|
|
||||||
}
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
|
||||||
|
|
||||||
# description
|
|
||||||
f.set_widget("description", "notes")
|
|
||||||
|
|
||||||
def get_xref_buttons(self, quantity_type):
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
buttons = []
|
|
||||||
|
|
||||||
if wf_quantity_type := (
|
|
||||||
session.query(model.QuantityType)
|
|
||||||
.filter(model.QuantityType.farmos_uuid == quantity_type["uuid"])
|
|
||||||
.first()
|
|
||||||
):
|
|
||||||
buttons.append(
|
|
||||||
self.make_button(
|
|
||||||
f"View {self.app.get_title()} record",
|
|
||||||
primary=True,
|
|
||||||
url=self.request.route_url(
|
|
||||||
"quantity_types.view", uuid=wf_quantity_type.uuid
|
|
||||||
),
|
|
||||||
icon_left="eye",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityMasterView(FarmOSMasterView):
|
|
||||||
"""
|
|
||||||
Base class for Quantity views
|
|
||||||
"""
|
|
||||||
|
|
||||||
farmos_quantity_type = None
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"measure",
|
|
||||||
"value",
|
|
||||||
"label",
|
|
||||||
"changed",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = ("changed", "desc")
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"measure",
|
|
||||||
"value",
|
|
||||||
"units",
|
|
||||||
"label",
|
|
||||||
"created",
|
|
||||||
"changed",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_grid_data(self, columns=None, session=None):
|
|
||||||
result = self.farmos_client.resource.get("quantity", self.farmos_quantity_type)
|
|
||||||
return [self.normalize_quantity(t) for t in result["data"]]
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# value
|
|
||||||
g.set_link("value")
|
|
||||||
|
|
||||||
# changed
|
|
||||||
g.set_renderer("changed", "datetime")
|
|
||||||
|
|
||||||
def get_instance(self):
|
|
||||||
quantity = self.farmos_client.resource.get_id(
|
|
||||||
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
|
|
||||||
)
|
|
||||||
self.raw_json = quantity
|
|
||||||
|
|
||||||
data = self.normalize_quantity(quantity["data"])
|
|
||||||
|
|
||||||
if relationships := quantity["data"].get("relationships"):
|
|
||||||
|
|
||||||
# add units
|
|
||||||
if units := relationships.get("units"):
|
|
||||||
if units["data"]:
|
|
||||||
unit = self.farmos_client.resource.get_id(
|
|
||||||
"taxonomy_term", "unit", units["data"]["id"]
|
|
||||||
)
|
|
||||||
data["units"] = {
|
|
||||||
"uuid": unit["data"]["id"],
|
|
||||||
"name": unit["data"]["attributes"]["name"],
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_instance_title(self, quantity):
|
|
||||||
return quantity["value"]
|
|
||||||
|
|
||||||
def normalize_quantity(self, quantity):
|
|
||||||
|
|
||||||
if created := quantity["attributes"]["created"]:
|
|
||||||
created = datetime.datetime.fromisoformat(created)
|
|
||||||
created = self.app.localtime(created)
|
|
||||||
|
|
||||||
if changed := quantity["attributes"]["changed"]:
|
|
||||||
changed = datetime.datetime.fromisoformat(changed)
|
|
||||||
changed = self.app.localtime(changed)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"uuid": quantity["id"],
|
|
||||||
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
|
||||||
"measure": quantity["attributes"]["measure"],
|
|
||||||
"value": quantity["attributes"]["value"],
|
|
||||||
"label": quantity["attributes"]["label"] or colander.null,
|
|
||||||
"created": created,
|
|
||||||
"changed": changed,
|
|
||||||
}
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
|
||||||
|
|
||||||
# created
|
|
||||||
f.set_node("created", WuttaDateTime(self.request))
|
|
||||||
f.set_widget("created", WuttaDateTimeWidget(self.request))
|
|
||||||
|
|
||||||
# changed
|
|
||||||
f.set_node("changed", WuttaDateTime(self.request))
|
|
||||||
f.set_widget("changed", WuttaDateTimeWidget(self.request))
|
|
||||||
|
|
||||||
# units
|
|
||||||
f.set_node("units", FarmOSUnitRef())
|
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantityView(QuantityMasterView):
|
|
||||||
"""
|
|
||||||
View for farmOS Standard Quantities
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_name = "farmos_standard_quantity"
|
|
||||||
model_title = "farmOS Standard Quantity"
|
|
||||||
model_title_plural = "farmOS Standard Quantities"
|
|
||||||
|
|
||||||
route_prefix = "farmos_quantities_standard"
|
|
||||||
url_prefix = "/farmOS/quantities/standard"
|
|
||||||
|
|
||||||
farmos_quantity_type = "standard"
|
|
||||||
|
|
||||||
def get_xref_buttons(self, standard_quantity):
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
buttons = []
|
|
||||||
|
|
||||||
if wf_standard_quantity := (
|
|
||||||
session.query(model.StandardQuantity)
|
|
||||||
.join(model.Quantity)
|
|
||||||
.filter(model.Quantity.farmos_uuid == standard_quantity["uuid"])
|
|
||||||
.first()
|
|
||||||
):
|
|
||||||
buttons.append(
|
|
||||||
self.make_button(
|
|
||||||
f"View {self.app.get_title()} record",
|
|
||||||
primary=True,
|
|
||||||
url=self.request.route_url(
|
|
||||||
"quantities_standard.view", uuid=wf_standard_quantity.uuid
|
|
||||||
),
|
|
||||||
icon_left="eye",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
|
||||||
base = globals()
|
|
||||||
|
|
||||||
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
|
|
||||||
QuantityTypeView.defaults(config)
|
|
||||||
|
|
||||||
StandardQuantityView = kwargs.get(
|
|
||||||
"StandardQuantityView", base["StandardQuantityView"]
|
|
||||||
)
|
|
||||||
StandardQuantityView.defaults(config)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
defaults(config)
|
|
||||||
|
|
@ -39,11 +39,11 @@ class StructureView(FarmOSMasterView):
|
||||||
View for farmOS Structures
|
View for farmOS Structures
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_name = "farmos_structure_asset"
|
model_name = "farmos_structure"
|
||||||
model_title = "farmOS Structure"
|
model_title = "farmOS Structure"
|
||||||
model_title_plural = "farmOS Structures"
|
model_title_plural = "farmOS Structures"
|
||||||
|
|
||||||
route_prefix = "farmos_structure_assets"
|
route_prefix = "farmos_structures"
|
||||||
url_prefix = "/farmOS/structures"
|
url_prefix = "/farmOS/structures"
|
||||||
|
|
||||||
farmos_refurl_path = "/assets/structure"
|
farmos_refurl_path = "/assets/structure"
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
View for farmOS units
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
|
||||||
|
|
||||||
|
|
||||||
class UnitView(TaxonomyMasterView):
|
|
||||||
"""
|
|
||||||
Master view for Units in farmOS.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_name = "farmos_unit"
|
|
||||||
model_title = "farmOS Unit"
|
|
||||||
model_title_plural = "farmOS Units"
|
|
||||||
|
|
||||||
route_prefix = "farmos_units"
|
|
||||||
url_prefix = "/farmOS/units"
|
|
||||||
|
|
||||||
farmos_taxonomy_type = "unit"
|
|
||||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
|
|
||||||
|
|
||||||
def get_xref_buttons(self, unit):
|
|
||||||
buttons = super().get_xref_buttons(unit)
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
|
|
||||||
if wf_unit := (
|
|
||||||
session.query(model.Unit)
|
|
||||||
.filter(model.Unit.farmos_uuid == unit["uuid"])
|
|
||||||
.first()
|
|
||||||
):
|
|
||||||
buttons.append(
|
|
||||||
self.make_button(
|
|
||||||
f"View {self.app.get_title()} record",
|
|
||||||
primary=True,
|
|
||||||
url=self.request.route_url("units.view", uuid=wf_unit.uuid),
|
|
||||||
icon_left="eye",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
|
||||||
base = globals()
|
|
||||||
|
|
||||||
UnitView = kwargs.get("UnitView", base["UnitView"])
|
|
||||||
UnitView.defaults(config)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
defaults(config)
|
|
||||||
|
|
@ -175,21 +175,15 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
Base class for Asset master views
|
Base class for Asset master views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
labels = {
|
|
||||||
"message": "Log Name",
|
|
||||||
"owners": "Owner",
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"status",
|
"status",
|
||||||
"drupal_id",
|
"drupal_id",
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"message",
|
"message",
|
||||||
"assets",
|
"assets",
|
||||||
# "location",
|
"location",
|
||||||
"quantity",
|
"quantity",
|
||||||
"is_group_assignment",
|
"is_group_assignment",
|
||||||
"owners",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = ("timestamp", "desc")
|
sort_defaults = ("timestamp", "desc")
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ from webhelpers2.html import tags
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
from wuttafarm.web.util import use_farmos_style_grid_links
|
|
||||||
|
|
||||||
|
|
||||||
class WuttaFarmMasterView(MasterView):
|
class WuttaFarmMasterView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
|
@ -51,10 +49,6 @@ class WuttaFarmMasterView(MasterView):
|
||||||
"thumbnail_url": "Thumbnail URL",
|
"thumbnail_url": "Thumbnail URL",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
|
||||||
super().__init__(request, context=context)
|
|
||||||
self.farmos_style_grid_links = use_farmos_style_grid_links(self.config)
|
|
||||||
|
|
||||||
def get_farmos_url(self, obj):
|
def get_farmos_url(self, obj):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -105,4 +99,4 @@ class WuttaFarmMasterView(MasterView):
|
||||||
|
|
||||||
def persist(self, obj, session=None):
|
def persist(self, obj, session=None):
|
||||||
super().persist(obj, session)
|
super().persist(obj, session)
|
||||||
self.app.auto_sync_to_farmos(obj, require=False)
|
self.app.export_to_farmos(obj, require=False)
|
||||||
|
|
|
||||||
|
|
@ -183,17 +183,8 @@ class PlantAssetView(AssetMasterView):
|
||||||
plant = f.model_instance
|
plant = f.model_instance
|
||||||
|
|
||||||
# plant_types
|
# plant_types
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("plant_types") # TODO: add support for this
|
|
||||||
else:
|
|
||||||
f.set_node("plant_types", PlantTypeRefs(self.request))
|
f.set_node("plant_types", PlantTypeRefs(self.request))
|
||||||
f.set_default(
|
f.set_default("plant_types", [t.plant_type_uuid for t in plant._plant_types])
|
||||||
"plant_types", [t.plant_type_uuid for t in plant._plant_types]
|
|
||||||
)
|
|
||||||
|
|
||||||
# season
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("season") # TODO: add support for this
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Master view for Quantities
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from wuttaweb.db import Session
|
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
|
||||||
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
|
|
||||||
from wuttafarm.web.forms.schema import UnitRef
|
|
||||||
|
|
||||||
|
|
||||||
def get_quantity_type_enum(config):
|
|
||||||
app = config.get_app()
|
|
||||||
model = app.model
|
|
||||||
session = Session()
|
|
||||||
quantity_types = OrderedDict()
|
|
||||||
query = session.query(model.QuantityType).order_by(model.QuantityType.name)
|
|
||||||
for quantity_type in query:
|
|
||||||
quantity_types[quantity_type.drupal_id] = quantity_type.name
|
|
||||||
return quantity_types
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityTypeView(WuttaFarmMasterView):
|
|
||||||
"""
|
|
||||||
Master view for Quantity Types
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = QuantityType
|
|
||||||
route_prefix = "quantity_types"
|
|
||||||
url_prefix = "/quantity-types"
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = "name"
|
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
"name": {"active": True, "verb": "contains"},
|
|
||||||
}
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# name
|
|
||||||
g.set_link("name")
|
|
||||||
|
|
||||||
def get_xref_buttons(self, quantity_type):
|
|
||||||
buttons = super().get_xref_buttons(quantity_type)
|
|
||||||
|
|
||||||
if quantity_type.farmos_uuid:
|
|
||||||
buttons.append(
|
|
||||||
self.make_button(
|
|
||||||
"View farmOS record",
|
|
||||||
primary=True,
|
|
||||||
url=self.request.route_url(
|
|
||||||
"farmos_quantity_types.view", uuid=quantity_type.farmos_uuid
|
|
||||||
),
|
|
||||||
icon_left="eye",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityMasterView(WuttaFarmMasterView):
|
|
||||||
"""
|
|
||||||
Base class for Quantity master views
|
|
||||||
"""
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"drupal_id",
|
|
||||||
"as_text",
|
|
||||||
"quantity_type",
|
|
||||||
"measure",
|
|
||||||
"value",
|
|
||||||
"units",
|
|
||||||
"label",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = ("drupal_id", "desc")
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"quantity_type",
|
|
||||||
"as_text",
|
|
||||||
"measure",
|
|
||||||
"value",
|
|
||||||
"units",
|
|
||||||
"label",
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_query(self, session=None):
|
|
||||||
""" """
|
|
||||||
model = self.app.model
|
|
||||||
model_class = self.get_model_class()
|
|
||||||
session = session or self.Session()
|
|
||||||
query = session.query(model_class)
|
|
||||||
if model_class is not model.Quantity:
|
|
||||||
query = query.join(model.Quantity)
|
|
||||||
query = query.join(model.Measure).join(model.Unit)
|
|
||||||
return query
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
model = self.app.model
|
|
||||||
model_class = self.get_model_class()
|
|
||||||
|
|
||||||
# drupal_id
|
|
||||||
g.set_label("drupal_id", "ID", column_only=True)
|
|
||||||
g.set_sorter("drupal_id", model.Quantity.drupal_id)
|
|
||||||
|
|
||||||
# as_text
|
|
||||||
g.set_renderer("as_text", self.render_as_text_for_grid)
|
|
||||||
g.set_link("as_text")
|
|
||||||
|
|
||||||
# quantity_type
|
|
||||||
if model_class is not model.Quantity:
|
|
||||||
g.remove("quantity_type")
|
|
||||||
else:
|
|
||||||
g.set_enum("quantity_type", get_quantity_type_enum(self.config))
|
|
||||||
|
|
||||||
# measure
|
|
||||||
g.set_sorter("measure", model.Measure.name)
|
|
||||||
|
|
||||||
# value
|
|
||||||
g.set_renderer("value", self.render_value_for_grid)
|
|
||||||
|
|
||||||
# units
|
|
||||||
g.set_sorter("units", model.Unit.name)
|
|
||||||
|
|
||||||
# label
|
|
||||||
g.set_sorter("label", model.Quantity.label)
|
|
||||||
|
|
||||||
# view action links to final quantity record
|
|
||||||
if model_class is model.Quantity:
|
|
||||||
|
|
||||||
def quantity_url(quantity, i):
|
|
||||||
return self.request.route_url(
|
|
||||||
f"quantities_{quantity.quantity_type_id}.view", uuid=quantity.uuid
|
|
||||||
)
|
|
||||||
|
|
||||||
g.add_action("view", icon="eye", url=quantity_url)
|
|
||||||
|
|
||||||
def render_as_text_for_grid(self, quantity, field, value):
|
|
||||||
return quantity.render_as_text(self.config)
|
|
||||||
|
|
||||||
def render_value_for_grid(self, quantity, field, value):
|
|
||||||
value = quantity.value_numerator / quantity.value_denominator
|
|
||||||
return self.app.render_quantity(value)
|
|
||||||
|
|
||||||
def get_instance_title(self, quantity):
|
|
||||||
return quantity.render_as_text(self.config)
|
|
||||||
|
|
||||||
def configure_form(self, form):
|
|
||||||
f = form
|
|
||||||
super().configure_form(f)
|
|
||||||
quantity = form.model_instance
|
|
||||||
|
|
||||||
# as_text
|
|
||||||
if self.creating or self.editing:
|
|
||||||
f.remove("as_text")
|
|
||||||
else:
|
|
||||||
f.set_default("as_text", quantity.render_as_text(self.config))
|
|
||||||
|
|
||||||
# quantity_type
|
|
||||||
if self.creating:
|
|
||||||
f.remove("quantity_type")
|
|
||||||
else:
|
|
||||||
f.set_readonly("quantity_type")
|
|
||||||
f.set_default("quantity_type", quantity.quantity_type.name)
|
|
||||||
|
|
||||||
# measure
|
|
||||||
if self.creating:
|
|
||||||
f.remove("measure")
|
|
||||||
else:
|
|
||||||
f.set_readonly("measure")
|
|
||||||
f.set_default("measure", quantity.measure.name)
|
|
||||||
|
|
||||||
# value
|
|
||||||
if self.creating:
|
|
||||||
f.remove("value")
|
|
||||||
else:
|
|
||||||
value = quantity.value_numerator / quantity.value_denominator
|
|
||||||
value = self.app.render_quantity(value)
|
|
||||||
f.set_default(
|
|
||||||
"value",
|
|
||||||
f"{value} ({quantity.value_numerator} / {quantity.value_denominator})",
|
|
||||||
)
|
|
||||||
|
|
||||||
# units
|
|
||||||
if self.creating:
|
|
||||||
f.remove("units")
|
|
||||||
else:
|
|
||||||
f.set_readonly("units")
|
|
||||||
f.set_node("units", UnitRef(self.request))
|
|
||||||
# TODO: ugh
|
|
||||||
f.set_default("units", quantity.quantity.units)
|
|
||||||
|
|
||||||
def get_xref_buttons(self, quantity):
|
|
||||||
buttons = super().get_xref_buttons(quantity)
|
|
||||||
|
|
||||||
if quantity.farmos_uuid:
|
|
||||||
url = self.request.route_url(
|
|
||||||
f"farmos_quantities_{quantity.quantity_type_id}.view",
|
|
||||||
uuid=quantity.farmos_uuid,
|
|
||||||
)
|
|
||||||
buttons.append(
|
|
||||||
self.make_button(
|
|
||||||
"View farmOS record", primary=True, url=url, icon_left="eye"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
|
|
||||||
|
|
||||||
class QuantityView(QuantityMasterView):
|
|
||||||
"""
|
|
||||||
Master view for All Quantities
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = Quantity
|
|
||||||
route_prefix = "quantities"
|
|
||||||
url_prefix = "/quantities"
|
|
||||||
|
|
||||||
viewable = False
|
|
||||||
creatable = False
|
|
||||||
editable = False
|
|
||||||
deletable = False
|
|
||||||
model_is_versioned = False
|
|
||||||
|
|
||||||
|
|
||||||
class StandardQuantityView(QuantityMasterView):
|
|
||||||
"""
|
|
||||||
Master view for Standard Quantities
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = StandardQuantity
|
|
||||||
route_prefix = "quantities_standard"
|
|
||||||
url_prefix = "/quantities/standard"
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
|
||||||
base = globals()
|
|
||||||
|
|
||||||
QuantityTypeView = kwargs.get("QuantityTypeView", base["QuantityTypeView"])
|
|
||||||
QuantityTypeView.defaults(config)
|
|
||||||
|
|
||||||
QuantityView = kwargs.get("QuantityView", base["QuantityView"])
|
|
||||||
QuantityView.defaults(config)
|
|
||||||
|
|
||||||
StandardQuantityView = kwargs.get(
|
|
||||||
"StandardQuantityView", base["StandardQuantityView"]
|
|
||||||
)
|
|
||||||
StandardQuantityView.defaults(config)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
defaults(config)
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Quick Form views for farmOS
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .base import QuickFormView
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
config.include("wuttafarm.web.views.quick.eggs")
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Base class for Quick Form views
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from pyramid.renderers import render_to_response
|
|
||||||
|
|
||||||
from wuttaweb.views import View
|
|
||||||
|
|
||||||
from wuttafarm.web.util import save_farmos_oauth2_token
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class QuickFormView(View):
|
|
||||||
"""
|
|
||||||
Base class for quick form views.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
|
||||||
super().__init__(request, context=context)
|
|
||||||
self.farmos_client = self.get_farmos_client()
|
|
||||||
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
|
||||||
self.normal = self.app.get_normalizer(self.farmos_client)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_route_slug(cls):
|
|
||||||
return cls.route_slug
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_url_slug(cls):
|
|
||||||
return cls.url_slug
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_form_title(cls):
|
|
||||||
return cls.form_title
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
form = self.make_quick_form()
|
|
||||||
|
|
||||||
if form.validate():
|
|
||||||
try:
|
|
||||||
result = self.save_quick_form(form)
|
|
||||||
except Exception as err:
|
|
||||||
log.warning("failed to save 'edit' form", exc_info=True)
|
|
||||||
self.request.session.flash(
|
|
||||||
f"Save failed: {self.app.render_error(err)}", "error"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return self.redirect_after_save(result)
|
|
||||||
|
|
||||||
return self.render_to_response({"form": form})
|
|
||||||
|
|
||||||
def make_quick_form(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def save_quick_form(self, form):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def redirect_after_save(self, result):
|
|
||||||
return self.redirect(self.request.current_route_url())
|
|
||||||
|
|
||||||
def render_to_response(self, context):
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
"index_title": "Quick Form",
|
|
||||||
"form_title": self.get_form_title(),
|
|
||||||
"help_text": self.__doc__.strip(),
|
|
||||||
}
|
|
||||||
|
|
||||||
defaults.update(context)
|
|
||||||
context = defaults
|
|
||||||
|
|
||||||
# supplement context further if needed
|
|
||||||
context = self.get_template_context(context)
|
|
||||||
|
|
||||||
page_templates = self.get_page_templates()
|
|
||||||
mako_path = page_templates[0]
|
|
||||||
try:
|
|
||||||
render_to_response(mako_path, context, request=self.request)
|
|
||||||
except IOError:
|
|
||||||
|
|
||||||
# try one or more fallback templates
|
|
||||||
for fallback in page_templates[1:]:
|
|
||||||
try:
|
|
||||||
return render_to_response(fallback, context, request=self.request)
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# if we made it all the way here, then we found no
|
|
||||||
# templates at all, in which case re-attempt the first and
|
|
||||||
# let that error raise on up
|
|
||||||
return render_to_response(mako_path, context, request=self.request)
|
|
||||||
|
|
||||||
def get_page_templates(self):
|
|
||||||
route_slug = self.get_route_slug()
|
|
||||||
page_templates = [f"/quick/{route_slug}.mako"]
|
|
||||||
page_templates.extend(self.get_fallback_templates())
|
|
||||||
return page_templates
|
|
||||||
|
|
||||||
def get_fallback_templates(self):
|
|
||||||
return ["/quick/form.mako"]
|
|
||||||
|
|
||||||
def get_template_context(self, context):
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_farmos_client(self):
|
|
||||||
token = self.request.session.get("farmos.oauth2.token")
|
|
||||||
if not token:
|
|
||||||
raise self.forbidden()
|
|
||||||
|
|
||||||
# nb. must give a *copy* of the token to farmOS client, since
|
|
||||||
# it will mutate it in-place and we don't want that to happen
|
|
||||||
# for our original copy in the user session. (otherwise the
|
|
||||||
# auto-refresh will not work correctly for subsequent calls.)
|
|
||||||
token = dict(token)
|
|
||||||
|
|
||||||
def token_updater(token):
|
|
||||||
save_farmos_oauth2_token(self.request, token)
|
|
||||||
|
|
||||||
return self.app.get_farmos_client(token=token, token_updater=token_updater)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def defaults(cls, config):
|
|
||||||
cls._defaults(config)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _defaults(cls, config):
|
|
||||||
route_slug = cls.get_route_slug()
|
|
||||||
url_slug = cls.get_url_slug()
|
|
||||||
|
|
||||||
config.add_route(f"quick.{route_slug}", f"/quick/{url_slug}")
|
|
||||||
config.add_view(cls, route_name=f"quick.{route_slug}")
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Quick Form for "Eggs"
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import colander
|
|
||||||
from deform.widget import SelectWidget
|
|
||||||
|
|
||||||
from farmOS.subrequests import Action, Subrequest, SubrequestsBlueprint, Format
|
|
||||||
|
|
||||||
from wuttaweb.forms.schema import WuttaDateTime
|
|
||||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
|
||||||
|
|
||||||
from wuttafarm.web.views.quick import QuickFormView
|
|
||||||
|
|
||||||
|
|
||||||
class EggsQuickForm(QuickFormView):
|
|
||||||
"""
|
|
||||||
Use this form to record an egg harvest. A harvest log will be
|
|
||||||
created with standard details filled in.
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_title = "Eggs"
|
|
||||||
route_slug = "eggs"
|
|
||||||
url_slug = "eggs"
|
|
||||||
|
|
||||||
_layer_assets = None
|
|
||||||
|
|
||||||
def make_quick_form(self):
|
|
||||||
f = self.make_form(
|
|
||||||
fields=[
|
|
||||||
"timestamp",
|
|
||||||
"count",
|
|
||||||
"asset",
|
|
||||||
"notes",
|
|
||||||
],
|
|
||||||
labels={
|
|
||||||
"timestamp": "Date",
|
|
||||||
"count": "Quantity",
|
|
||||||
"asset": "Layer Asset",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# timestamp
|
|
||||||
f.set_node("timestamp", WuttaDateTime())
|
|
||||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
|
||||||
f.set_default("timestamp", self.app.make_utc())
|
|
||||||
|
|
||||||
# count
|
|
||||||
f.set_node("count", colander.Integer())
|
|
||||||
|
|
||||||
# asset
|
|
||||||
assets = self.get_layer_assets()
|
|
||||||
values = [(a["uuid"], a["name"]) for a in assets]
|
|
||||||
f.set_widget("asset", SelectWidget(values=values))
|
|
||||||
if len(assets) == 1:
|
|
||||||
f.set_default("asset", assets[0]["uuid"])
|
|
||||||
|
|
||||||
# notes
|
|
||||||
f.set_widget("notes", "notes")
|
|
||||||
f.set_required("notes", False)
|
|
||||||
|
|
||||||
return f
|
|
||||||
|
|
||||||
def get_layer_assets(self):
|
|
||||||
if self._layer_assets is not None:
|
|
||||||
return self._layer_assets
|
|
||||||
|
|
||||||
assets = []
|
|
||||||
params = {
|
|
||||||
"filter[produces_eggs]": 1,
|
|
||||||
"sort": "name",
|
|
||||||
}
|
|
||||||
|
|
||||||
def normalize(asset):
|
|
||||||
return {
|
|
||||||
"uuid": asset["id"],
|
|
||||||
"name": asset["attributes"]["name"],
|
|
||||||
"type": asset["type"],
|
|
||||||
}
|
|
||||||
|
|
||||||
result = self.farmos_client.asset.get("animal", params=params)
|
|
||||||
assets.extend([normalize(a) for a in result["data"]])
|
|
||||||
|
|
||||||
result = self.farmos_client.asset.get("group", params=params)
|
|
||||||
assets.extend([normalize(a) for a in result["data"]])
|
|
||||||
|
|
||||||
assets.sort(key=lambda a: a["name"])
|
|
||||||
self._layer_assets = assets
|
|
||||||
return assets
|
|
||||||
|
|
||||||
def save_quick_form(self, form):
|
|
||||||
|
|
||||||
response = self.save_to_farmos(form)
|
|
||||||
log = json.loads(response["create-log#body{0}"]["body"])
|
|
||||||
|
|
||||||
if self.app.is_farmos_mirror():
|
|
||||||
quantity = json.loads(response["create-quantity"]["body"])
|
|
||||||
self.app.auto_sync_from_farmos(quantity["data"], "StandardQuantity")
|
|
||||||
self.app.auto_sync_from_farmos(log["data"], "HarvestLog")
|
|
||||||
|
|
||||||
return log
|
|
||||||
|
|
||||||
def save_to_farmos(self, form):
|
|
||||||
data = form.validated
|
|
||||||
|
|
||||||
assets = self.get_layer_assets()
|
|
||||||
assets = {a["uuid"]: a for a in assets}
|
|
||||||
asset = assets[data["asset"]]
|
|
||||||
|
|
||||||
# TODO: make this configurable?
|
|
||||||
unit_name = "egg(s)"
|
|
||||||
|
|
||||||
unit = {"data": {"type": "taxonomy_term--unit"}}
|
|
||||||
new_unit = None
|
|
||||||
|
|
||||||
result = self.farmos_client.resource.get(
|
|
||||||
"taxonomy_term",
|
|
||||||
"unit",
|
|
||||||
params={
|
|
||||||
"filter[name]": unit_name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if result["data"]:
|
|
||||||
unit["data"]["id"] = result["data"][0]["id"]
|
|
||||||
else:
|
|
||||||
payload = dict(unit)
|
|
||||||
payload["data"]["attributes"] = {"name": unit_name}
|
|
||||||
new_unit = Subrequest(
|
|
||||||
action=Action.create,
|
|
||||||
requestId="create-unit",
|
|
||||||
endpoint="api/taxonomy_term/unit",
|
|
||||||
body=payload,
|
|
||||||
)
|
|
||||||
unit["data"]["id"] = "{{create-unit.body@$.data.id}}"
|
|
||||||
|
|
||||||
quantity = {
|
|
||||||
"data": {
|
|
||||||
"type": "quantity--standard",
|
|
||||||
"attributes": {
|
|
||||||
"measure": "count",
|
|
||||||
"value": {
|
|
||||||
"numerator": data["count"],
|
|
||||||
"denominator": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"units": unit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
kw = {}
|
|
||||||
if new_unit:
|
|
||||||
kw["waitFor"] = ["create-unit"]
|
|
||||||
new_quantity = Subrequest(
|
|
||||||
action=Action.create,
|
|
||||||
requestId="create-quantity",
|
|
||||||
endpoint="api/quantity/standard",
|
|
||||||
body=quantity,
|
|
||||||
**kw,
|
|
||||||
)
|
|
||||||
|
|
||||||
notes = None
|
|
||||||
if data["notes"]:
|
|
||||||
notes = {"value": data["notes"]}
|
|
||||||
|
|
||||||
log = {
|
|
||||||
"data": {
|
|
||||||
"type": "log--harvest",
|
|
||||||
"attributes": {
|
|
||||||
"name": f"Collected {data['count']} {unit_name}",
|
|
||||||
"notes": notes,
|
|
||||||
"quick": ["eggs"],
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"asset": {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": asset["uuid"],
|
|
||||||
"type": asset["type"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"quantity": {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "{{create-quantity.body@$.data.id}}",
|
|
||||||
"type": "quantity--standard",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
new_log = Subrequest(
|
|
||||||
action=Action.create,
|
|
||||||
requestId="create-log",
|
|
||||||
waitFor=["create-quantity"],
|
|
||||||
endpoint="api/log/harvest",
|
|
||||||
body=log,
|
|
||||||
)
|
|
||||||
|
|
||||||
blueprints = [new_quantity, new_log]
|
|
||||||
if new_unit:
|
|
||||||
blueprints.insert(0, new_unit)
|
|
||||||
blueprint = SubrequestsBlueprint.parse_obj(blueprints)
|
|
||||||
response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def redirect_after_save(self, result):
|
|
||||||
return self.redirect(
|
|
||||||
self.request.route_url(
|
|
||||||
"farmos_logs_harvest.view", uuid=result["data"]["id"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
EggsQuickForm.defaults(config)
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Custom views for Settings
|
|
||||||
"""
|
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from wuttaweb.views import settings as base
|
|
||||||
|
|
||||||
from wuttafarm.web.util import use_farmos_style_grid_links
|
|
||||||
|
|
||||||
|
|
||||||
class AppInfoView(base.AppInfoView):
|
|
||||||
"""
|
|
||||||
Custom appinfo view
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_appinfo_dict(self):
|
|
||||||
info = super().get_appinfo_dict()
|
|
||||||
enum = self.app.enum
|
|
||||||
|
|
||||||
mode = self.config.get(
|
|
||||||
f"{self.app.appname}.farmos_integration_mode", default="wrapper"
|
|
||||||
)
|
|
||||||
|
|
||||||
info["farmos_integration"] = {
|
|
||||||
"label": "farmOS Integration",
|
|
||||||
"value": enum.FARMOS_INTEGRATION_MODE.get(mode, mode),
|
|
||||||
}
|
|
||||||
|
|
||||||
url = self.app.get_farmos_url()
|
|
||||||
info["farmos_url"] = {
|
|
||||||
"label": "farmOS URL",
|
|
||||||
"value": tags.link_to(url, url, target="_blank"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
def configure_get_simple_settings(self): # pylint: disable=empty-docstring
|
|
||||||
simple_settings = super().configure_get_simple_settings()
|
|
||||||
simple_settings.extend(
|
|
||||||
[
|
|
||||||
{"name": "farmos.url.base"},
|
|
||||||
{
|
|
||||||
"name": f"{self.app.appname}.farmos_integration_mode",
|
|
||||||
"default": self.app.get_farmos_integration_mode(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": f"{self.app.appname}.farmos_style_grid_links",
|
|
||||||
"type": bool,
|
|
||||||
"default": use_farmos_style_grid_links(self.config),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return simple_settings
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
|
||||||
local = globals()
|
|
||||||
AppInfoView = kwargs.get("AppInfoView", local["AppInfoView"])
|
|
||||||
base.defaults(config, **{"AppInfoView": AppInfoView})
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
defaults(config)
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
|
||||||
# Copyright © 2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of WuttaFarm.
|
|
||||||
#
|
|
||||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Master view for Units
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
|
||||||
from wuttafarm.db.model import Measure, Unit
|
|
||||||
|
|
||||||
|
|
||||||
class MeasureView(WuttaFarmMasterView):
|
|
||||||
"""
|
|
||||||
Master view for Measures
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = Measure
|
|
||||||
route_prefix = "measures"
|
|
||||||
url_prefix = "/measures"
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"name",
|
|
||||||
"drupal_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = "name"
|
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
"name": {"active": True, "verb": "contains"},
|
|
||||||
}
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"name",
|
|
||||||
"drupal_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# name
|
|
||||||
g.set_link("name")
|
|
||||||
|
|
||||||
|
|
||||||
class UnitView(WuttaFarmMasterView):
|
|
||||||
"""
|
|
||||||
Master view for Units
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_class = Unit
|
|
||||||
route_prefix = "units"
|
|
||||||
url_prefix = "/units"
|
|
||||||
|
|
||||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
sort_defaults = "name"
|
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
"name": {"active": True, "verb": "contains"},
|
|
||||||
}
|
|
||||||
|
|
||||||
form_fields = [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"farmos_uuid",
|
|
||||||
"drupal_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
|
||||||
g = grid
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# name
|
|
||||||
g.set_link("name")
|
|
||||||
|
|
||||||
def get_farmos_url(self, unit):
|
|
||||||
return self.app.get_farmos_url(f"/taxonomy/term/{unit.drupal_id}")
|
|
||||||
|
|
||||||
def get_xref_buttons(self, unit):
|
|
||||||
buttons = super().get_xref_buttons(unit)
|
|
||||||
|
|
||||||
if unit.farmos_uuid:
|
|
||||||
buttons.append(
|
|
||||||
self.make_button(
|
|
||||||
"View farmOS record",
|
|
||||||
primary=True,
|
|
||||||
url=self.request.route_url(
|
|
||||||
"farmos_units.view", uuid=unit.farmos_uuid
|
|
||||||
),
|
|
||||||
icon_left="eye",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
|
||||||
base = globals()
|
|
||||||
|
|
||||||
MeasureView = kwargs.get("MeasureView", base["MeasureView"])
|
|
||||||
MeasureView.defaults(config)
|
|
||||||
|
|
||||||
UnitView = kwargs.get("UnitView", base["UnitView"])
|
|
||||||
UnitView.defaults(config)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
defaults(config)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue