Compare commits
No commits in common. "e27dad573b84fe262a8267ca498412476d28f1af" and "a5b699a52ab7f92cae7ef87e2d5eff62d547880c" have entirely different histories.
e27dad573b
...
a5b699a52a
7 changed files with 19 additions and 167 deletions
|
|
@ -5,13 +5,6 @@ All notable changes to WuttaFarm will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.11.3 (2026-05-04)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- set material types when creating new log w/ material quantity
|
|
||||||
- split numerator/denominator for quantity values
|
|
||||||
|
|
||||||
## v0.11.2 (2026-03-21)
|
## v0.11.2 (2026-03-21)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ include:
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Documentation:
|
:caption: Documentation:
|
||||||
|
|
||||||
narr/intro
|
|
||||||
narr/install
|
narr/install
|
||||||
narr/auth
|
narr/auth
|
||||||
narr/features
|
narr/features
|
||||||
|
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
|
|
||||||
==============
|
|
||||||
Introduction
|
|
||||||
==============
|
|
||||||
|
|
||||||
This page hopefully conveys the what and why of WuttaFarm as a project.
|
|
||||||
|
|
||||||
WuttaFarm can serve as a "supplement" or "alternative" to `farmOS
|
|
||||||
<https://farmos.org>`_, depending on your needs. It has 3 distinct
|
|
||||||
"modes" of operation:
|
|
||||||
|
|
||||||
* :ref:`wrapper (API only) <wrapper-mode>`
|
|
||||||
* :ref:`mirror (2-way sync) <mirror-mode>`
|
|
||||||
* :ref:`standalone <standalone-mode>`
|
|
||||||
|
|
||||||
|
|
||||||
Rationale
|
|
||||||
---------
|
|
||||||
|
|
||||||
This project exists primarily to scratch a personal itch, but the hope
|
|
||||||
of course is that others will find it useful also. This is partly why
|
|
||||||
the app has different integration modes.
|
|
||||||
|
|
||||||
Realistically this is not (yet?) meant to be a "finished" app per se.
|
|
||||||
In that sense it is akin to farmOS itself, i.e. "part app, part
|
|
||||||
framework" which can be extended where needed.
|
|
||||||
|
|
||||||
It purposefully does not try to re-invent every wheel provided by
|
|
||||||
farmOS. But it does re-invent "certain types" of wheels, to some
|
|
||||||
degree, depending on which mode is used.
|
|
||||||
|
|
||||||
It should also be mentioned, the 3 "modes" are essentially just
|
|
||||||
abstractions for sake of convenience. The framework allows one to
|
|
||||||
customize however you want regardless of the "mode" in effect.
|
|
||||||
|
|
||||||
The target audience I suppose, is folks like me who prefer Python over
|
|
||||||
PHP, and can customize away without having to touch Drupal.
|
|
||||||
|
|
||||||
|
|
||||||
.. _wrapper-mode:
|
|
||||||
|
|
||||||
Wrapper Mode (API only)
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
This is the default integration mode for the app. In wrapper mode,
|
|
||||||
WuttaFarm UI will only expose views which display/modify farmOS data
|
|
||||||
*directly* via its JSONAPI. The app will not expose any views which
|
|
||||||
operate on its "native" tables (for assets, logs etc.).
|
|
||||||
|
|
||||||
Any data model extensions needed must of course be managed on the
|
|
||||||
farmOS side, since the native tables are not used for this mode.
|
|
||||||
|
|
||||||
This mode is simplest (for farmOS integration) since every user action
|
|
||||||
goes immeditely through the API. There is no "sync" or caching
|
|
||||||
involved.
|
|
||||||
|
|
||||||
There is one known issue with this mode, and it's kind of a bummer:
|
|
||||||
|
|
||||||
When viewing the "list" page for any record type (e.g. Animal Assets),
|
|
||||||
due to a quirk with the JSONAPI, you can only see the "first 50"
|
|
||||||
results in the listing. However you can still filter/sort the
|
|
||||||
records, to hopefully find what you need. And you can still view
|
|
||||||
*any* record if you have its URL (which you can normally figure out
|
|
||||||
from the farmOS UUID value).
|
|
||||||
|
|
||||||
|
|
||||||
.. _mirror-mode:
|
|
||||||
|
|
||||||
Mirror Mode (2-way sync)
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
This mode exposes the native table views in addition to direct API
|
|
||||||
views. It's sort of a combination of the wrapper and standalone
|
|
||||||
modes, with an extra daemon process to handle some of the sync tasks.
|
|
||||||
|
|
||||||
(It's also the most complex mode to setup; more "moving parts" which
|
|
||||||
means potentially more to go wrong and require troubleshooting.)
|
|
||||||
|
|
||||||
Again this depends on your actual use case, but a "complete" setup for
|
|
||||||
this mode would mean:
|
|
||||||
|
|
||||||
* changes made in WuttaFarm are synced to farmOS via JSONAPI
|
|
||||||
* changes made in farmOS are synced to WuttaFarm via webhook+daemon
|
|
||||||
|
|
||||||
The latter includes changes made via the farmOS proper web app, and/or
|
|
||||||
changes made via the "direct API" views within WuttaFarm (or anything
|
|
||||||
else that invokes the farmOS API). This requires the extra daemon
|
|
||||||
process (running alongside the WuttaFarm web app process), as well as
|
|
||||||
the `webhooks module
|
|
||||||
<https://farmos.discourse.group/t/using-the-webhooks-module/2490>`_ on
|
|
||||||
the farmOS side.
|
|
||||||
|
|
||||||
It's possible to extend the data model on the WuttaFarm side with or
|
|
||||||
without doing so on the farmOS side, and vice versa. Although
|
|
||||||
regardless you may have to take both into consideration for your
|
|
||||||
overall sync needs.
|
|
||||||
|
|
||||||
There is support for "full" data import/export between systems in both
|
|
||||||
directions, WuttaFarm <=> farmOS. Limited of course by the common
|
|
||||||
schema they share etc. This can be leveraged easily to include
|
|
||||||
nightly "data diff" checks, ensuring all stays in sync.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. _standalone-mode:
|
|
||||||
|
|
||||||
Standalone Mode
|
|
||||||
---------------
|
|
||||||
|
|
||||||
This mode exposes *only* the native table views, and none of the
|
|
||||||
"direct API" views. This requires no farmOS at all.
|
|
||||||
|
|
||||||
This lets you customize the data model as needed, using SQLAlchemy and
|
|
||||||
Alembic, as does the mirror mode. However no "sync" is required so
|
|
||||||
you don't have to consider the farmOS data model.
|
|
||||||
|
|
||||||
While the wrapper mode is simplest for farmOS integration, this
|
|
||||||
standalone mode is even simpler, technically speaking. It should work
|
|
||||||
well if you just need the basic record-keeping aspects.
|
|
||||||
|
|
||||||
The main downside with this mode is there is not (yet?) any
|
|
||||||
mapping/geometry support, nor is there support for image/file
|
|
||||||
attachments.
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaFarm"
|
name = "WuttaFarm"
|
||||||
version = "0.11.3"
|
version = "0.11.2"
|
||||||
description = "Web app to integrate with and extend farmOS"
|
description = "Web app to integrate with and extend farmOS"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|
|
||||||
|
|
@ -138,11 +138,6 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "land_types",
|
"route": "land_types",
|
||||||
"perm": "land_types.list",
|
"perm": "land_types.list",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Material Types",
|
|
||||||
"route": "material_types",
|
|
||||||
"perm": "material_types.list",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "Plant Types",
|
"title": "Plant Types",
|
||||||
"route": "plant_types",
|
"route": "plant_types",
|
||||||
|
|
@ -223,6 +218,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
||||||
"route": "log_types",
|
"route": "log_types",
|
||||||
"perm": "log_types.list",
|
"perm": "log_types.list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Material Types",
|
||||||
|
"route": "material_types",
|
||||||
|
"perm": "material_types.list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Measures",
|
"title": "Measures",
|
||||||
"route": "measures",
|
"route": "measures",
|
||||||
|
|
|
||||||
|
|
@ -847,18 +847,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const measureMap = {}
|
const measureMap = {}
|
||||||
for (const m of this.measures) {
|
for (let m of this.measures) {
|
||||||
measureMap[m.drupal_id] = m.name
|
measureMap[m.drupal_id] = m.name
|
||||||
}
|
}
|
||||||
|
|
||||||
const quantityTypeMap = {}
|
|
||||||
for (const qt of this.quantityTypes) {
|
|
||||||
quantityTypeMap[qt.drupal_id] = qt.name
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
measureMap,
|
measureMap,
|
||||||
quantityTypeMap,
|
|
||||||
quantityType: this.defaultQuantityType,
|
quantityType: this.defaultQuantityType,
|
||||||
editShowDialog: false,
|
editShowDialog: false,
|
||||||
editNew: true,
|
editNew: true,
|
||||||
|
|
@ -899,7 +893,8 @@
|
||||||
|
|
||||||
this.newQuantity.quantity_type = {
|
this.newQuantity.quantity_type = {
|
||||||
drupal_id: this.quantityType,
|
drupal_id: this.quantityType,
|
||||||
name: this.quantityTypeMap[this.quantityType],
|
## TODO: add support for other quantity types
|
||||||
|
name: "Standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newQuantity.measure = null
|
this.newQuantity.measure = null
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@
|
||||||
Base views for Logs
|
Base views for Logs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import decimal
|
|
||||||
import time
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
|
@ -400,16 +398,13 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
units = session.get(model.Unit, new_qty["units"]["uuid"])
|
units = session.get(model.Unit, new_qty["units"]["uuid"])
|
||||||
assert units
|
assert units
|
||||||
if new_qty["uuid"].startswith("new_"):
|
if new_qty["uuid"].startswith("new_"):
|
||||||
num, denom = decimal.Decimal(new_qty["value"]).as_integer_ratio()
|
|
||||||
qty = self.app.make_true_quantity(
|
qty = self.app.make_true_quantity(
|
||||||
new_qty["quantity_type"]["drupal_id"],
|
new_qty["quantity_type"]["drupal_id"],
|
||||||
measure_id=new_qty["measure"],
|
measure_id=new_qty["measure"],
|
||||||
value_numerator=num,
|
value_numerator=int(new_qty["value"]),
|
||||||
value_denominator=denom,
|
value_denominator=1,
|
||||||
units=units,
|
units=units,
|
||||||
)
|
)
|
||||||
if qty.quantity_type_id == "material":
|
|
||||||
self.set_material_types(qty, new_qty["material_types"])
|
|
||||||
# nb. must ensure "typed" quantity record persists!
|
# nb. must ensure "typed" quantity record persists!
|
||||||
session.add(qty)
|
session.add(qty)
|
||||||
# but must add "generic" quantity record to log
|
# but must add "generic" quantity record to log
|
||||||
|
|
@ -446,19 +441,12 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
if old_mtype.uuid.hex not in desired:
|
if old_mtype.uuid.hex not in desired:
|
||||||
quantity.material_types.remove(old_mtype)
|
quantity.material_types.remove(old_mtype)
|
||||||
|
|
||||||
def auto_sync_to_farmos(self, client, uuid):
|
def auto_sync_to_farmos(self, client, log):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
model_class = self.get_model_class()
|
session = self.Session()
|
||||||
|
|
||||||
with self.app.short_session(commit=True) as session:
|
# nb. ensure quantities have uuid keys
|
||||||
if user := session.query(model.User).filter_by(username="farmos").first():
|
session.flush()
|
||||||
session.info["continuum_user_id"] = user.uuid
|
|
||||||
|
|
||||||
log = None
|
|
||||||
while not log:
|
|
||||||
log = session.get(model_class, uuid)
|
|
||||||
if not log:
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
for qty in log.quantities:
|
for qty in log.quantities:
|
||||||
qty = self.app.get_true_quantity(qty)
|
qty = self.app.get_true_quantity(qty)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue