Compare commits
18 commits
5ee2db267a
...
e7ef5c3d32
| Author | SHA1 | Date | |
|---|---|---|---|
| e7ef5c3d32 | |||
| 1a6870b8fe | |||
| ad6ac13d50 | |||
| c976d94bdd | |||
| 5d7dea5a84 | |||
| e5e3d38365 | |||
| 1af2b695dc | |||
| bbb1207b27 | |||
| 9cfa91e091 | |||
| 87101d6b04 | |||
| 1f254ca775 | |||
| d884a761ad | |||
| cfe2e4b7b4 | |||
| c93660ec4a | |||
| 0a0d43aa9f | |||
| bc0836fc3c | |||
| e7b493d7c9 | |||
| 185cd86efb |
45 changed files with 4783 additions and 530 deletions
|
|
@ -51,6 +51,44 @@ class WuttaFarmAppHandler(base.AppHandler):
|
|||
self.handlers["farmos"] = factory(self.config)
|
||||
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):
|
||||
"""
|
||||
Get a farmOS URL. This is a convenience wrapper around
|
||||
|
|
@ -85,7 +123,20 @@ class WuttaFarmAppHandler(base.AppHandler):
|
|||
handler = self.get_farmos_handler()
|
||||
return handler.is_farmos_4x(*args, **kwargs)
|
||||
|
||||
def export_to_farmos(self, obj, require=True):
|
||||
def get_normalizer(self, farmos_client=None):
|
||||
"""
|
||||
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.
|
||||
|
||||
|
|
@ -103,7 +154,8 @@ class WuttaFarmAppHandler(base.AppHandler):
|
|||
"""
|
||||
handler = self.app.get_import_handler("export.to_farmos.from_wuttafarm")
|
||||
|
||||
model_name = type(obj).__name__
|
||||
if not model_name:
|
||||
model_name = type(obj).__name__
|
||||
if model_name not in handler.importers:
|
||||
if require:
|
||||
raise ValueError(f"no exporter found for {model_name}")
|
||||
|
|
@ -117,6 +169,37 @@ class WuttaFarmAppHandler(base.AppHandler):
|
|||
normal = importer.normalize_source_object(obj)
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class WuttaFarmConfig(WuttaConfigExtension):
|
|||
|
||||
# web app menu
|
||||
config.setdefault(
|
||||
f"{config.appname}.web.menus.handler.spec",
|
||||
f"{config.appname}.web.menus.handler.default_spec",
|
||||
"wuttafarm.web.menus:WuttaFarmMenuHandler",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
"""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")
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
"""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")
|
||||
102
src/wuttafarm/db/alembic/versions/ea88e72a5fa5_add_units.py
Normal file
102
src/wuttafarm/db/alembic/versions/ea88e72a5fa5_add_units.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""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,6 +30,8 @@ from wuttjamaican.db.model import *
|
|||
from .users import WuttaFarmUser
|
||||
|
||||
# wuttafarm proper models
|
||||
from .unit import Unit, Measure
|
||||
from .quantities import QuantityType, Quantity, StandardQuantity
|
||||
from .asset import AssetType, Asset, AssetParent
|
||||
from .asset_land import LandType, LandAsset
|
||||
from .asset_structure import StructureType, StructureAsset
|
||||
|
|
|
|||
221
src/wuttafarm/db/model/quantities.py
Normal file
221
src/wuttafarm/db/model/quantities.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# -*- 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)
|
||||
117
src/wuttafarm/db/model/unit.py
Normal file
117
src/wuttafarm/db/model/unit.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# -*- 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,6 +28,19 @@ from collections import OrderedDict
|
|||
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(
|
||||
[
|
||||
("M", "Male"),
|
||||
|
|
|
|||
|
|
@ -64,6 +64,81 @@ class ToFarmOS(Importer):
|
|||
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):
|
||||
"""
|
||||
Base class for asset data importer targeting the farmOS API.
|
||||
|
|
@ -151,6 +226,12 @@ class ToFarmOSAsset(ToFarmOS):
|
|||
return payload
|
||||
|
||||
|
||||
class UnitImporter(ToFarmOSTaxonomy):
|
||||
|
||||
model_title = "Unit"
|
||||
farmos_taxonomy_type = "unit"
|
||||
|
||||
|
||||
class AnimalAssetImporter(ToFarmOSAsset):
|
||||
|
||||
model_title = "AnimalAsset"
|
||||
|
|
@ -209,77 +290,10 @@ class AnimalAssetImporter(ToFarmOSAsset):
|
|||
return payload
|
||||
|
||||
|
||||
class AnimalTypeImporter(ToFarmOS):
|
||||
class AnimalTypeImporter(ToFarmOSTaxonomy):
|
||||
|
||||
model_title = "AnimalType"
|
||||
|
||||
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"])
|
||||
farmos_taxonomy_type = "animal_type"
|
||||
|
||||
|
||||
class GroupAssetImporter(ToFarmOSAsset):
|
||||
|
|
@ -333,6 +347,59 @@ class LandAssetImporter(ToFarmOSAsset):
|
|||
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):
|
||||
|
||||
model_title = "StructureAsset"
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
|||
importers["AnimalType"] = AnimalTypeImporter
|
||||
importers["AnimalAsset"] = AnimalAssetImporter
|
||||
importers["GroupAsset"] = GroupAssetImporter
|
||||
importers["PlantAsset"] = PlantAssetImporter
|
||||
importers["Unit"] = UnitImporter
|
||||
importers["ActivityLog"] = ActivityLogImporter
|
||||
importers["HarvestLog"] = HarvestLogImporter
|
||||
importers["MedicalLog"] = MedicalLogImporter
|
||||
|
|
@ -183,6 +185,28 @@ 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):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Group Assets
|
||||
|
|
@ -239,6 +263,32 @@ 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(
|
||||
FromWuttaFarm, farmos_importing.model.StructureAssetImporter
|
||||
):
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class FromFarmOSHandler(ImportHandler):
|
|||
token = self.get_farmos_oauth2_token()
|
||||
self.farmos_client = self.app.get_farmos_client(token=token)
|
||||
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):
|
||||
|
||||
|
|
@ -76,6 +77,7 @@ class FromFarmOSHandler(ImportHandler):
|
|||
kwargs = super().get_importer_kwargs(key, **kwargs)
|
||||
kwargs["farmos_client"] = self.farmos_client
|
||||
kwargs["farmos_4x"] = self.farmos_4x
|
||||
kwargs["normal"] = self.normal
|
||||
return kwargs
|
||||
|
||||
|
||||
|
|
@ -106,6 +108,10 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
|
|||
importers["GroupAsset"] = GroupAssetImporter
|
||||
importers["PlantType"] = PlantTypeImporter
|
||||
importers["PlantAsset"] = PlantAssetImporter
|
||||
importers["Measure"] = MeasureImporter
|
||||
importers["Unit"] = UnitImporter
|
||||
importers["QuantityType"] = QuantityTypeImporter
|
||||
importers["StandardQuantity"] = StandardQuantityImporter
|
||||
importers["LogType"] = LogTypeImporter
|
||||
importers["ActivityLog"] = ActivityLogImporter
|
||||
importers["HarvestLog"] = HarvestLogImporter
|
||||
|
|
@ -821,6 +827,95 @@ 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):
|
||||
"""
|
||||
farmOS API → WuttaFarm importer for Log Types
|
||||
|
|
@ -888,33 +983,25 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
|||
def get_source_objects(self):
|
||||
""" """
|
||||
log_type = self.get_farmos_log_type()
|
||||
result = self.farmos_client.log.get(log_type)
|
||||
return result["data"]
|
||||
|
||||
def get_asset_type(self, asset):
|
||||
return asset["type"].split("--")[1]
|
||||
return list(self.farmos_client.log.iterate(log_type))
|
||||
|
||||
def normalize_source_object(self, log):
|
||||
""" """
|
||||
if notes := log["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
data = self.normal.normalize_farmos_log(log)
|
||||
|
||||
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:
|
||||
assets = []
|
||||
for asset in log["relationships"]["asset"]["data"]:
|
||||
assets.append((self.get_asset_type(asset), UUID(asset["id"])))
|
||||
data["assets"] = [
|
||||
(a["asset_type"], UUID(a["uuid"])) for a in data["assets"]
|
||||
]
|
||||
|
||||
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,
|
||||
}
|
||||
return data
|
||||
|
||||
def normalize_target_object(self, log):
|
||||
data = super().normalize_target_object(log)
|
||||
|
|
@ -1040,3 +1127,134 @@ class ObservationLogImporter(LogImporterBase):
|
|||
"status",
|
||||
"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",
|
||||
]
|
||||
|
|
|
|||
199
src/wuttafarm/normal.py
Normal file
199
src/wuttafarm/normal.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# -*- 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,6 +55,135 @@ class AnimalTypeRef(ObjectRef):
|
|||
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):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
|
|
@ -179,6 +308,27 @@ class StructureTypeRef(ObjectRef):
|
|||
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):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -26,12 +26,14 @@ Custom form widgets for WuttaFarm
|
|||
import json
|
||||
|
||||
import colander
|
||||
from deform.widget import Widget
|
||||
from deform.widget import Widget, SelectWidget
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
||||
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
|
||||
from wuttaweb.db import Session
|
||||
|
||||
from wuttafarm.web.util import render_quantity_objects
|
||||
|
||||
|
||||
class ImageWidget(Widget):
|
||||
"""
|
||||
|
|
@ -54,6 +56,172 @@ class ImageWidget(Widget):
|
|||
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):
|
||||
"""
|
||||
Widget to display an "animal type" field.
|
||||
|
|
@ -162,7 +330,7 @@ class StructureWidget(Widget):
|
|||
return tags.link_to(
|
||||
structure["name"],
|
||||
self.request.route_url(
|
||||
"farmos_structures.view", uuid=structure["uuid"]
|
||||
"farmos_structure_assets.view", uuid=structure["uuid"]
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
300
src/wuttafarm/web/grids.py
Normal file
300
src/wuttafarm/web/grids.py
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
# -*- 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,12 +32,50 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"""
|
||||
|
||||
def make_menus(self, request, **kwargs):
|
||||
return [
|
||||
self.make_asset_menu(request),
|
||||
self.make_log_menu(request),
|
||||
self.make_farmos_menu(request),
|
||||
self.make_admin_menu(request, include_people=True),
|
||||
]
|
||||
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 [
|
||||
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_log_menu(request),
|
||||
self.make_farmos_full_menu(request),
|
||||
admin_menu,
|
||||
]
|
||||
|
||||
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):
|
||||
return {
|
||||
|
|
@ -134,15 +172,41 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"perm": "logs_observation.list",
|
||||
},
|
||||
{"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",
|
||||
"route": "log_types",
|
||||
"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_menu(self, request):
|
||||
def make_farmos_full_menu(self, request):
|
||||
config = request.wutta_config
|
||||
app = config.get_app()
|
||||
return {
|
||||
|
|
@ -156,30 +220,30 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
},
|
||||
{"type": "sep"},
|
||||
{
|
||||
"title": "Animals",
|
||||
"route": "farmos_animals",
|
||||
"perm": "farmos_animals.list",
|
||||
"title": "Animal Assets",
|
||||
"route": "farmos_animal_assets",
|
||||
"perm": "farmos_animal_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Groups",
|
||||
"route": "farmos_groups",
|
||||
"perm": "farmos_groups.list",
|
||||
"title": "Group Assets",
|
||||
"route": "farmos_group_assets",
|
||||
"perm": "farmos_group_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Plants",
|
||||
"route": "farmos_asset_plant",
|
||||
"perm": "farmos_asset_plant.list",
|
||||
},
|
||||
{
|
||||
"title": "Structures",
|
||||
"route": "farmos_structures",
|
||||
"perm": "farmos_structures.list",
|
||||
},
|
||||
{
|
||||
"title": "Land",
|
||||
"title": "Land Assets",
|
||||
"route": "farmos_land_assets",
|
||||
"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"},
|
||||
{
|
||||
"title": "Activity Logs",
|
||||
|
|
@ -207,6 +271,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"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",
|
||||
|
|
@ -217,11 +286,7 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"route": "farmos_structure_types",
|
||||
"perm": "farmos_structure_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Land Types",
|
||||
"route": "farmos_land_types",
|
||||
"perm": "farmos_land_types.list",
|
||||
},
|
||||
{"type": "sep"},
|
||||
{
|
||||
"title": "Asset Types",
|
||||
"route": "farmos_asset_types",
|
||||
|
|
@ -232,6 +297,155 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"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",
|
||||
},
|
||||
{"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"},
|
||||
{
|
||||
"title": "Users",
|
||||
|
|
|
|||
49
src/wuttafarm/web/templates/appinfo/configure.mako
Normal file
49
src/wuttafarm/web/templates/appinfo/configure.mako
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
## -*- 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>
|
||||
14
src/wuttafarm/web/templates/quick/form.mako
Normal file
14
src/wuttafarm/web/templates/quick/form.mako
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<%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,6 +23,8 @@
|
|||
Misc. utilities for web app
|
||||
"""
|
||||
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
|
||||
def save_farmos_oauth2_token(request, token):
|
||||
"""
|
||||
|
|
@ -38,3 +40,22 @@ def save_farmos_oauth2_token(request, token):
|
|||
|
||||
# save token to user session
|
||||
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,6 +29,10 @@ from .master import WuttaFarmMasterView
|
|||
|
||||
|
||||
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
|
||||
essential.defaults(
|
||||
|
|
@ -36,23 +40,32 @@ def includeme(config):
|
|||
**{
|
||||
"wuttaweb.views.auth": "wuttafarm.web.views.auth",
|
||||
"wuttaweb.views.common": "wuttafarm.web.views.common",
|
||||
"wuttaweb.views.settings": "wuttafarm.web.views.settings",
|
||||
"wuttaweb.views.users": "wuttafarm.web.views.users",
|
||||
}
|
||||
)
|
||||
|
||||
# native table views
|
||||
config.include("wuttafarm.web.views.asset_types")
|
||||
config.include("wuttafarm.web.views.assets")
|
||||
config.include("wuttafarm.web.views.land")
|
||||
config.include("wuttafarm.web.views.structures")
|
||||
config.include("wuttafarm.web.views.animals")
|
||||
config.include("wuttafarm.web.views.groups")
|
||||
config.include("wuttafarm.web.views.plants")
|
||||
config.include("wuttafarm.web.views.logs")
|
||||
config.include("wuttafarm.web.views.logs_activity")
|
||||
config.include("wuttafarm.web.views.logs_harvest")
|
||||
config.include("wuttafarm.web.views.logs_medical")
|
||||
config.include("wuttafarm.web.views.logs_observation")
|
||||
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.assets")
|
||||
config.include("wuttafarm.web.views.land")
|
||||
config.include("wuttafarm.web.views.structures")
|
||||
config.include("wuttafarm.web.views.animals")
|
||||
config.include("wuttafarm.web.views.groups")
|
||||
config.include("wuttafarm.web.views.plants")
|
||||
config.include("wuttafarm.web.views.logs")
|
||||
config.include("wuttafarm.web.views.logs_activity")
|
||||
config.include("wuttafarm.web.views.logs_harvest")
|
||||
config.include("wuttafarm.web.views.logs_medical")
|
||||
config.include("wuttafarm.web.views.logs_observation")
|
||||
|
||||
# quick form views
|
||||
# (nb. these work with all integration modes)
|
||||
config.include("wuttafarm.web.views.quick")
|
||||
|
||||
# views for farmOS
|
||||
config.include("wuttafarm.web.views.farmos")
|
||||
if mode != enum.FARMOS_INTEGRATION_MODE_NONE:
|
||||
config.include("wuttafarm.web.views.farmos")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
Master view for Animals
|
||||
"""
|
||||
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDictEnum
|
||||
|
||||
from wuttafarm.db.model import AnimalType, AnimalAsset
|
||||
|
|
@ -153,11 +155,11 @@ class AnimalAssetView(AssetMasterView):
|
|||
"thumbnail",
|
||||
"drupal_id",
|
||||
"asset_name",
|
||||
"produces_eggs",
|
||||
"animal_type",
|
||||
"birthdate",
|
||||
"is_sterile",
|
||||
"sex",
|
||||
"produces_eggs",
|
||||
"archived",
|
||||
]
|
||||
|
||||
|
|
@ -165,9 +167,9 @@ class AnimalAssetView(AssetMasterView):
|
|||
"asset_name",
|
||||
"animal_type",
|
||||
"birthdate",
|
||||
"produces_eggs",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"produces_eggs",
|
||||
"notes",
|
||||
"asset_type",
|
||||
"archived",
|
||||
|
|
@ -189,6 +191,10 @@ class AnimalAssetView(AssetMasterView):
|
|||
g.set_joiner("animal_type", lambda q: q.join(model.AnimalType))
|
||||
g.set_sorter("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
|
||||
g.set_renderer("birthdate", "date")
|
||||
|
|
@ -196,6 +202,10 @@ class AnimalAssetView(AssetMasterView):
|
|||
# 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):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
|
|
|||
|
|
@ -278,29 +278,14 @@ class AssetMasterView(WuttaFarmMasterView):
|
|||
buttons = super().get_xref_buttons(asset)
|
||||
|
||||
if asset.farmos_uuid:
|
||||
|
||||
# TODO
|
||||
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(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(route, uuid=asset.farmos_uuid),
|
||||
icon_left="eye",
|
||||
)
|
||||
asset_type = self.get_model_class().__wutta_hint__["farmos_asset_type"]
|
||||
route = f"farmos_{asset_type}_assets.view"
|
||||
url = self.request.route_url(route, uuid=asset.farmos_uuid)
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record", primary=True, url=url, icon_left="eye"
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ class CommonView(base.CommonView):
|
|||
site_admin = session.query(model.Role).filter_by(name="Site Admin").first()
|
||||
if site_admin:
|
||||
site_admin_perms = [
|
||||
"activity_logs.list",
|
||||
"activity_logs.view",
|
||||
"activity_logs.versions",
|
||||
"animal_types.create",
|
||||
"animal_types.edit",
|
||||
"animal_types.list",
|
||||
|
|
@ -68,14 +65,14 @@ class CommonView(base.CommonView):
|
|||
"asset_types.list",
|
||||
"asset_types.view",
|
||||
"asset_types.versions",
|
||||
"farmos_animal_assets.list",
|
||||
"farmos_animal_assets.view",
|
||||
"farmos_animal_types.list",
|
||||
"farmos_animal_types.view",
|
||||
"farmos_animals.list",
|
||||
"farmos_animals.view",
|
||||
"farmos_asset_types.list",
|
||||
"farmos_asset_types.view",
|
||||
"farmos_groups.list",
|
||||
"farmos_groups.view",
|
||||
"farmos_group_assets.list",
|
||||
"farmos_group_assets.view",
|
||||
"farmos_land_assets.list",
|
||||
"farmos_land_assets.view",
|
||||
"farmos_land_types.list",
|
||||
|
|
@ -84,17 +81,23 @@ class CommonView(base.CommonView):
|
|||
"farmos_log_types.view",
|
||||
"farmos_logs_activity.list",
|
||||
"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.view",
|
||||
"farmos_structures.list",
|
||||
"farmos_structures.view",
|
||||
"farmos_users.list",
|
||||
"farmos_users.view",
|
||||
"group_asests.create",
|
||||
"group_asests.edit",
|
||||
"group_asests.list",
|
||||
"group_asests.view",
|
||||
"group_asests.versions",
|
||||
"group_assets.create",
|
||||
"group_assets.edit",
|
||||
"group_assets.list",
|
||||
"group_assets.view",
|
||||
"group_assets.versions",
|
||||
"land_assets.create",
|
||||
"land_assets.edit",
|
||||
"land_assets.list",
|
||||
|
|
@ -106,6 +109,18 @@ class CommonView(base.CommonView):
|
|||
"log_types.list",
|
||||
"log_types.view",
|
||||
"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.view",
|
||||
"structure_types.versions",
|
||||
|
|
@ -114,6 +129,11 @@ class CommonView(base.CommonView):
|
|||
"structure_assets.list",
|
||||
"structure_assets.view",
|
||||
"structure_assets.versions",
|
||||
"units.create",
|
||||
"units.edit",
|
||||
"units.list",
|
||||
"units.view",
|
||||
"units.versions",
|
||||
]
|
||||
for perm in site_admin_perms:
|
||||
auth.grant_permission(site_admin, perm)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ from .master import FarmOSMasterView
|
|||
|
||||
def includeme(config):
|
||||
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.units")
|
||||
config.include("wuttafarm.web.views.farmos.land_types")
|
||||
config.include("wuttafarm.web.views.farmos.land_assets")
|
||||
config.include("wuttafarm.web.views.farmos.structure_types")
|
||||
|
|
|
|||
|
|
@ -23,16 +23,10 @@
|
|||
View for farmOS animal types
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import colander
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDateTime
|
||||
|
||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
||||
|
||||
|
||||
class AnimalTypeView(FarmOSMasterView):
|
||||
class AnimalTypeView(TaxonomyMasterView):
|
||||
"""
|
||||
Master view for Animal Types in farmOS.
|
||||
"""
|
||||
|
|
@ -44,90 +38,14 @@ class AnimalTypeView(FarmOSMasterView):
|
|||
route_prefix = "farmos_animal_types"
|
||||
url_prefix = "/farmOS/animal-types"
|
||||
|
||||
farmos_taxonomy_type = "animal_type"
|
||||
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):
|
||||
buttons = super().get_xref_buttons(animal_type)
|
||||
model = self.app.model
|
||||
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 := (
|
||||
session.query(model.AnimalType)
|
||||
.filter(model.AnimalType.farmos_uuid == animal_type["uuid"])
|
||||
|
|
|
|||
|
|
@ -26,152 +26,159 @@ Master view for Farm Animals
|
|||
import datetime
|
||||
|
||||
import colander
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDateTime
|
||||
from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum
|
||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||
|
||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||
from wuttafarm.web.forms.schema import UsersType, AnimalTypeType, StructureType
|
||||
from wuttafarm.web.forms.widgets import ImageWidget
|
||||
from wuttafarm.web.views.farmos.assets import AssetMasterView
|
||||
from wuttafarm.web.grids import (
|
||||
SimpleSorter,
|
||||
StringFilter,
|
||||
BooleanFilter,
|
||||
NullableBooleanFilter,
|
||||
DateTimeFilter,
|
||||
)
|
||||
from wuttafarm.web.forms.schema import FarmOSRef
|
||||
|
||||
|
||||
class AnimalView(FarmOSMasterView):
|
||||
class AnimalView(AssetMasterView):
|
||||
"""
|
||||
Master view for Farm Animals
|
||||
"""
|
||||
|
||||
model_name = "farmos_animal"
|
||||
model_title = "farmOS Animal"
|
||||
model_title_plural = "farmOS Animals"
|
||||
model_name = "farmos_animal_assets"
|
||||
model_title = "farmOS Animal Asset"
|
||||
model_title_plural = "farmOS Animal Assets"
|
||||
|
||||
route_prefix = "farmos_animals"
|
||||
url_prefix = "/farmOS/animals"
|
||||
route_prefix = "farmos_animal_assets"
|
||||
url_prefix = "/farmOS/assets/animal"
|
||||
|
||||
farmos_asset_type = "animal"
|
||||
farmos_refurl_path = "/assets/animal"
|
||||
|
||||
labels = {
|
||||
"animal_type": "Species / Breed",
|
||||
"location": "Current Location",
|
||||
"animal_type_name": "Species / Breed",
|
||||
"is_sterile": "Sterile",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"thumbnail",
|
||||
"drupal_id",
|
||||
"name",
|
||||
"produces_eggs",
|
||||
"animal_type_name",
|
||||
"birthdate",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"sex",
|
||||
"groups",
|
||||
"owners",
|
||||
"locations",
|
||||
"archived",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"animal_type",
|
||||
"birthdate",
|
||||
"produces_eggs",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"archived",
|
||||
"owners",
|
||||
"location",
|
||||
"notes",
|
||||
"raw_image_url",
|
||||
"large_image_url",
|
||||
"thumbnail_image_url",
|
||||
"asset_type_name",
|
||||
"groups",
|
||||
"owners",
|
||||
"locations",
|
||||
"archived",
|
||||
"thumbnail_url",
|
||||
"image_url",
|
||||
"thumbnail",
|
||||
"image",
|
||||
]
|
||||
|
||||
def get_grid_data(self, columns=None, session=None):
|
||||
animals = self.farmos_client.resource.get("asset", "animal")
|
||||
return [self.normalize_animal(a) for a in animals["data"]]
|
||||
def get_farmos_api_includes(self):
|
||||
includes = super().get_farmos_api_includes()
|
||||
includes.add("animal_type")
|
||||
includes.add("group")
|
||||
return includes
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
enum = self.app.enum
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
g.set_searchable("name")
|
||||
# produces_eggs
|
||||
g.set_renderer("produces_eggs", "boolean")
|
||||
g.set_sorter("produces_eggs", SimpleSorter("produces_eggs"))
|
||||
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
|
||||
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
|
||||
g.set_renderer("is_sterile", "boolean")
|
||||
g.set_sorter("is_sterile", SimpleSorter("is_sterile"))
|
||||
g.set_filter("is_sterile", BooleanFilter)
|
||||
|
||||
# archived
|
||||
g.set_renderer("archived", "boolean")
|
||||
def render_animal_type_for_grid(self, animal, field, value):
|
||||
uuid = animal["animal_type"]["uuid"]
|
||||
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):
|
||||
|
||||
animal = self.farmos_client.resource.get_id(
|
||||
"asset", "animal", self.request.matchdict["uuid"]
|
||||
)
|
||||
self.raw_json = animal
|
||||
data = super().get_instance()
|
||||
|
||||
# instance data
|
||||
data = self.normalize_animal(animal["data"])
|
||||
|
||||
if relationships := animal["data"].get("relationships"):
|
||||
if relationships := self.raw_json["data"].get("relationships"):
|
||||
|
||||
# add animal type
|
||||
if animal_type := relationships.get("animal_type"):
|
||||
if animal_type["data"]:
|
||||
animal_type = self.farmos_client.resource.get_id(
|
||||
"taxonomy_term", "animal_type", animal_type["data"]["id"]
|
||||
)
|
||||
data["animal_type"] = {
|
||||
"uuid": animal_type["data"]["id"],
|
||||
"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"],
|
||||
if not data.get("animal_type"):
|
||||
if animal_type := relationships.get("animal_type"):
|
||||
if animal_type["data"]:
|
||||
animal_type = self.farmos_client.resource.get_id(
|
||||
"taxonomy_term", "animal_type", animal_type["data"]["id"]
|
||||
)
|
||||
data["animal_type"] = {
|
||||
"uuid": animal_type["data"]["id"],
|
||||
"name": animal_type["data"]["attributes"]["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
|
||||
|
||||
def get_instance_title(self, animal):
|
||||
return animal["name"]
|
||||
|
||||
def normalize_animal(self, animal):
|
||||
def normalize_asset(self, animal, included):
|
||||
normal = super().normalize_asset(animal, included)
|
||||
|
||||
birthdate = animal["attributes"]["birthdate"]
|
||||
if birthdate:
|
||||
|
|
@ -184,87 +191,138 @@ class AnimalView(FarmOSMasterView):
|
|||
else:
|
||||
sterile = animal["attributes"]["is_castrated"]
|
||||
|
||||
if notes := animal["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
animal_type_object = None
|
||||
group_objects = []
|
||||
group_names = []
|
||||
if relationships := animal.get("relationships"):
|
||||
|
||||
if self.farmos_4x:
|
||||
archived = animal["attributes"]["archived"]
|
||||
else:
|
||||
archived = animal["attributes"]["status"] == "archived"
|
||||
if animal_type := relationships.get("animal_type"):
|
||||
if animal_type := included.get(animal_type["data"]["id"]):
|
||||
animal_type_object = {
|
||||
"uuid": animal_type["id"],
|
||||
"name": animal_type["attributes"]["name"],
|
||||
}
|
||||
|
||||
return {
|
||||
"uuid": animal["id"],
|
||||
"drupal_id": animal["attributes"]["drupal_internal__id"],
|
||||
"name": animal["attributes"]["name"],
|
||||
"birthdate": birthdate,
|
||||
"sex": animal["attributes"]["sex"] or colander.null,
|
||||
"is_sterile": sterile,
|
||||
"location": colander.null, # TODO
|
||||
"archived": archived,
|
||||
"notes": notes or colander.null,
|
||||
}
|
||||
if groups := relationships.get("group"):
|
||||
for group in groups["data"]:
|
||||
if group := included.get(group["id"]):
|
||||
group = {
|
||||
"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,
|
||||
"sex": animal["attributes"]["sex"] or colander.null,
|
||||
"is_sterile": sterile,
|
||||
"produces_eggs": animal["attributes"].get("produces_eggs"),
|
||||
}
|
||||
)
|
||||
|
||||
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):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
enum = self.app.enum
|
||||
animal = f.model_instance
|
||||
|
||||
# animal_type
|
||||
f.set_node("animal_type", AnimalTypeType(self.request))
|
||||
f.set_node(
|
||||
"animal_type",
|
||||
FarmOSRef(
|
||||
self.request, "farmos_animal_types", values=self.get_animal_types
|
||||
),
|
||||
)
|
||||
|
||||
# produces_eggs
|
||||
f.set_node("produces_eggs", colander.Boolean())
|
||||
|
||||
# birthdate
|
||||
f.set_node("birthdate", WuttaDateTime())
|
||||
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
|
||||
f.set_node("is_sterile", colander.Boolean())
|
||||
|
||||
# location
|
||||
f.set_node("location", StructureType(self.request))
|
||||
# groups
|
||||
if self.creating or self.editing:
|
||||
f.remove("groups") # TODO
|
||||
|
||||
# owners
|
||||
f.set_node("owners", UsersType(self.request))
|
||||
def get_api_payload(self, animal):
|
||||
payload = super().get_api_payload(animal)
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
birthdate = None
|
||||
if animal["birthdate"]:
|
||||
birthdate = self.app.localtime(animal["birthdate"]).timestamp()
|
||||
|
||||
# archived
|
||||
f.set_node("archived", colander.Boolean())
|
||||
attrs = {
|
||||
"sex": animal["sex"] or None,
|
||||
"is_sterile": animal["is_sterile"],
|
||||
"produces_eggs": animal["produces_eggs"],
|
||||
"birthdate": birthdate,
|
||||
}
|
||||
|
||||
# image
|
||||
if url := animal.get("large_image_url"):
|
||||
f.set_widget("image", ImageWidget("animal image"))
|
||||
f.set_default("image", url)
|
||||
rels = {
|
||||
"animal_type": {
|
||||
"data": {
|
||||
"id": animal["animal_type"],
|
||||
"type": "taxonomy_term--animal_type",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload["attributes"].update(attrs)
|
||||
payload.setdefault("relationships", {}).update(rels)
|
||||
return payload
|
||||
|
||||
def get_xref_buttons(self, animal):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
buttons = super().get_xref_buttons(animal)
|
||||
|
||||
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 self.app.is_farmos_mirror():
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
if wf_animal := (
|
||||
session.query(model.Asset)
|
||||
.filter(model.Asset.farmos_uuid == animal["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"animal_assets.view", uuid=wf_animal.uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
if wf_animal := (
|
||||
session.query(model.Asset)
|
||||
.filter(model.Asset.farmos_uuid == animal["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"animal_assets.view", uuid=wf_animal.uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
|
|
|||
317
src/wuttafarm/web/views/farmos/assets.py
Normal file
317
src/wuttafarm/web/views/farmos/assets.py
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
# -*- 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_plural = "farmOS Groups"
|
||||
|
||||
route_prefix = "farmos_groups"
|
||||
route_prefix = "farmos_group_assets"
|
||||
url_prefix = "/farmOS/groups"
|
||||
|
||||
farmos_refurl_path = "/assets/group"
|
||||
|
|
|
|||
|
|
@ -23,14 +23,27 @@
|
|||
View for farmOS Harvest Logs
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from webhelpers2.html import tags
|
||||
|
||||
import colander
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDateTime
|
||||
from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum, Notes
|
||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||
|
||||
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):
|
||||
|
|
@ -39,75 +52,180 @@ class LogMasterView(FarmOSMasterView):
|
|||
"""
|
||||
|
||||
farmos_log_type = None
|
||||
filterable = True
|
||||
sort_on_backend = True
|
||||
|
||||
labels = {
|
||||
"name": "Log Name",
|
||||
"log_type_name": "Log Type",
|
||||
"quantities": "Quantity",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"timestamp",
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"quantities",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
"status": {"active": True, "verb": "not_equal", "value": "abandoned"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"timestamp",
|
||||
"status",
|
||||
"assets",
|
||||
"quantities",
|
||||
"notes",
|
||||
"status",
|
||||
"log_type_name",
|
||||
"owners",
|
||||
"quick",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
def get_grid_data(self, columns=None, session=None):
|
||||
result = self.farmos_client.log.get(self.farmos_log_type)
|
||||
return [self.normalize_log(l) for l in result["data"]]
|
||||
def get_farmos_api_includes(self):
|
||||
return {"log_type", "quantity", "asset", "owner"}
|
||||
|
||||
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):
|
||||
g = grid
|
||||
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
|
||||
g.set_link("name")
|
||||
g.set_searchable("name")
|
||||
g.set_sorter("name", SimpleSorter("name"))
|
||||
g.set_filter("name", StringFilter)
|
||||
|
||||
# timestamp
|
||||
g.set_renderer("timestamp", "datetime")
|
||||
# assets
|
||||
g.set_renderer("assets", self.render_assets_for_grid)
|
||||
|
||||
# quantities
|
||||
g.set_renderer("quantities", self.render_quantities_for_grid)
|
||||
|
||||
# is_group_assignment
|
||||
g.set_renderer("is_group_assignment", "boolean")
|
||||
g.set_sorter("is_group_assignment", 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):
|
||||
log = self.farmos_client.log.get_id(
|
||||
self.farmos_log_type, self.request.matchdict["uuid"]
|
||||
result = self.farmos_client.log.get_id(
|
||||
self.farmos_log_type,
|
||||
self.request.matchdict["uuid"],
|
||||
params={"include": ",".join(self.get_farmos_api_includes())},
|
||||
)
|
||||
self.raw_json = log
|
||||
return self.normalize_log(log["data"])
|
||||
self.raw_json = result
|
||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
||||
return self.normalize_log(result["data"], included)
|
||||
|
||||
def get_instance_title(self, log):
|
||||
return log["name"]
|
||||
|
||||
def normalize_log(self, log):
|
||||
|
||||
if timestamp := log["attributes"]["timestamp"]:
|
||||
timestamp = datetime.datetime.fromisoformat(timestamp)
|
||||
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,
|
||||
}
|
||||
def normalize_log(self, log, included):
|
||||
data = self.normal.normalize_farmos_log(log, included)
|
||||
data.update(
|
||||
{
|
||||
"log_type_name": data["log_type"].get("name"),
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
enum = self.app.enum
|
||||
log = f.model_instance
|
||||
|
||||
# timestamp
|
||||
f.set_node("timestamp", WuttaDateTime())
|
||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
||||
|
||||
# assets
|
||||
f.set_node("assets", FarmOSAssetRefs(self.request))
|
||||
|
||||
# quantities
|
||||
f.set_node("quantities", FarmOSQuantityRefs(self.request))
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
f.set_node("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):
|
||||
model = self.app.model
|
||||
|
|
|
|||
|
|
@ -41,6 +41,17 @@ class HarvestLogView(LogMasterView):
|
|||
farmos_log_type = "harvest"
|
||||
farmos_refurl_path = "/logs/harvest"
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"quantities",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
|||
|
|
@ -23,13 +23,25 @@
|
|||
Base class for farmOS master views
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import colander
|
||||
import markdown
|
||||
from webhelpers2.html import tags
|
||||
|
||||
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
|
||||
from wuttafarm.web.util import save_farmos_oauth2_token, use_farmos_style_grid_links
|
||||
from wuttafarm.web.grids import (
|
||||
ResourceData,
|
||||
StringFilter,
|
||||
NullableStringFilter,
|
||||
DateTimeFilter,
|
||||
SimpleSorter,
|
||||
)
|
||||
|
||||
|
||||
class FarmOSMasterView(MasterView):
|
||||
|
|
@ -50,6 +62,7 @@ class FarmOSMasterView(MasterView):
|
|||
farmos_refurl_path = None
|
||||
|
||||
labels = {
|
||||
"drupal_id": "Drupal ID",
|
||||
"raw_image_url": "Raw Image URL",
|
||||
"large_image_url": "Large Image URL",
|
||||
"thumbnail_image_url": "Thumbnail Image URL",
|
||||
|
|
@ -59,7 +72,9 @@ class FarmOSMasterView(MasterView):
|
|||
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)
|
||||
self.raw_json = None
|
||||
self.farmos_style_grid_links = use_farmos_style_grid_links(self.config)
|
||||
|
||||
def get_farmos_client(self):
|
||||
token = self.request.session.get("farmos.oauth2.token")
|
||||
|
|
@ -86,6 +101,16 @@ class FarmOSMasterView(MasterView):
|
|||
|
||||
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):
|
||||
|
||||
if self.listing and self.farmos_refurl_path:
|
||||
|
|
@ -100,3 +125,143 @@ class FarmOSMasterView(MasterView):
|
|||
)
|
||||
|
||||
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,12 +30,13 @@ import colander
|
|||
from wuttaweb.forms.schema import WuttaDateTime
|
||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||
|
||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||
from wuttafarm.web.forms.schema import UsersType, StructureType, FarmOSPlantTypes
|
||||
from wuttafarm.web.forms.widgets import ImageWidget
|
||||
|
||||
|
||||
class PlantTypeView(FarmOSMasterView):
|
||||
class PlantTypeView(TaxonomyMasterView):
|
||||
"""
|
||||
Master view for Plant Types in farmOS.
|
||||
"""
|
||||
|
|
@ -47,90 +48,14 @@ class PlantTypeView(FarmOSMasterView):
|
|||
route_prefix = "farmos_plant_types"
|
||||
url_prefix = "/farmOS/plant-types"
|
||||
|
||||
farmos_taxonomy_type = "plant_type"
|
||||
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):
|
||||
buttons = super().get_xref_buttons(plant_type)
|
||||
model = self.app.model
|
||||
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 := (
|
||||
session.query(model.PlantType)
|
||||
.filter(model.PlantType.farmos_uuid == plant_type["uuid"])
|
||||
|
|
@ -155,11 +80,11 @@ class PlantAssetView(FarmOSMasterView):
|
|||
Master view for farmOS Plant Assets
|
||||
"""
|
||||
|
||||
model_name = "farmos_asset_plant"
|
||||
model_name = "farmos_plant_assets"
|
||||
model_title = "farmOS Plant Asset"
|
||||
model_title_plural = "farmOS Plant Assets"
|
||||
|
||||
route_prefix = "farmos_asset_plant"
|
||||
route_prefix = "farmos_plant_assets"
|
||||
url_prefix = "/farmOS/assets/plant"
|
||||
|
||||
farmos_refurl_path = "/assets/plant"
|
||||
|
|
|
|||
278
src/wuttafarm/web/views/farmos/quantities.py
Normal file
278
src/wuttafarm/web/views/farmos/quantities.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# -*- 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
|
||||
"""
|
||||
|
||||
model_name = "farmos_structure"
|
||||
model_name = "farmos_structure_asset"
|
||||
model_title = "farmOS Structure"
|
||||
model_title_plural = "farmOS Structures"
|
||||
|
||||
route_prefix = "farmos_structures"
|
||||
route_prefix = "farmos_structure_assets"
|
||||
url_prefix = "/farmOS/structures"
|
||||
|
||||
farmos_refurl_path = "/assets/structure"
|
||||
|
|
|
|||
74
src/wuttafarm/web/views/farmos/units.py
Normal file
74
src/wuttafarm/web/views/farmos/units.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
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,15 +175,21 @@ class LogMasterView(WuttaFarmMasterView):
|
|||
Base class for Asset master views
|
||||
"""
|
||||
|
||||
labels = {
|
||||
"message": "Log Name",
|
||||
"owners": "Owner",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"message",
|
||||
"assets",
|
||||
"location",
|
||||
# "location",
|
||||
"quantity",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ from webhelpers2.html import tags
|
|||
|
||||
from wuttaweb.views import MasterView
|
||||
|
||||
from wuttafarm.web.util import use_farmos_style_grid_links
|
||||
|
||||
|
||||
class WuttaFarmMasterView(MasterView):
|
||||
"""
|
||||
|
|
@ -49,6 +51,10 @@ class WuttaFarmMasterView(MasterView):
|
|||
"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):
|
||||
return None
|
||||
|
||||
|
|
@ -99,4 +105,4 @@ class WuttaFarmMasterView(MasterView):
|
|||
|
||||
def persist(self, obj, session=None):
|
||||
super().persist(obj, session)
|
||||
self.app.export_to_farmos(obj, require=False)
|
||||
self.app.auto_sync_to_farmos(obj, require=False)
|
||||
|
|
|
|||
|
|
@ -183,8 +183,17 @@ class PlantAssetView(AssetMasterView):
|
|||
plant = f.model_instance
|
||||
|
||||
# plant_types
|
||||
f.set_node("plant_types", PlantTypeRefs(self.request))
|
||||
f.set_default("plant_types", [t.plant_type_uuid for t in plant._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_default(
|
||||
"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):
|
||||
|
|
|
|||
293
src/wuttafarm/web/views/quantities.py
Normal file
293
src/wuttafarm/web/views/quantities.py
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# -*- 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)
|
||||
30
src/wuttafarm/web/views/quick/__init__.py
Normal file
30
src/wuttafarm/web/views/quick/__init__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# -*- 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")
|
||||
156
src/wuttafarm/web/views/quick/base.py
Normal file
156
src/wuttafarm/web/views/quick/base.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# -*- 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}")
|
||||
243
src/wuttafarm/web/views/quick/eggs.py
Normal file
243
src/wuttafarm/web/views/quick/eggs.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# -*- 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)
|
||||
85
src/wuttafarm/web/views/settings.py
Normal file
85
src/wuttafarm/web/views/settings.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# -*- 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)
|
||||
131
src/wuttafarm/web/views/units.py
Normal file
131
src/wuttafarm/web/views/units.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# -*- 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