Compare commits
4 commits
a5b699a52a
...
e27dad573b
| Author | SHA1 | Date | |
|---|---|---|---|
| e27dad573b | |||
| 0ab511d841 | |||
| 066fcb857f | |||
| d9ed0de775 |
7 changed files with 167 additions and 19 deletions
|
|
@ -5,6 +5,13 @@ 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,6 +24,7 @@ include:
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Documentation:
|
:caption: Documentation:
|
||||||
|
|
||||||
|
narr/intro
|
||||||
narr/install
|
narr/install
|
||||||
narr/auth
|
narr/auth
|
||||||
narr/features
|
narr/features
|
||||||
|
|
|
||||||
123
docs/narr/intro.rst
Normal file
123
docs/narr/intro.rst
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
|
||||||
|
==============
|
||||||
|
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.2"
|
version = "0.11.3"
|
||||||
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,6 +138,11 @@ 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",
|
||||||
|
|
@ -218,11 +223,6 @@ 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,12 +847,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const measureMap = {}
|
const measureMap = {}
|
||||||
for (let m of this.measures) {
|
for (const 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,
|
||||||
|
|
@ -893,8 +899,7 @@
|
||||||
|
|
||||||
this.newQuantity.quantity_type = {
|
this.newQuantity.quantity_type = {
|
||||||
drupal_id: this.quantityType,
|
drupal_id: this.quantityType,
|
||||||
## TODO: add support for other quantity types
|
name: this.quantityTypeMap[this.quantityType],
|
||||||
name: "Standard",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newQuantity.measure = null
|
this.newQuantity.measure = null
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
Base views for Logs
|
Base views for Logs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import decimal
|
||||||
|
import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
|
@ -398,13 +400,16 @@ 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=int(new_qty["value"]),
|
value_numerator=num,
|
||||||
value_denominator=1,
|
value_denominator=denom,
|
||||||
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
|
||||||
|
|
@ -441,12 +446,19 @@ 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, log):
|
def auto_sync_to_farmos(self, client, uuid):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
session = self.Session()
|
model_class = self.get_model_class()
|
||||||
|
|
||||||
# nb. ensure quantities have uuid keys
|
with self.app.short_session(commit=True) as session:
|
||||||
session.flush()
|
if user := session.query(model.User).filter_by(username="farmos").first():
|
||||||
|
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