Compare commits
107 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af2ea18e1d | |||
| 23af35842d | |||
| 609a900f39 | |||
| a547188a90 | |||
| 81fa22bbd8 | |||
| b2b49d93ae | |||
| 7bffa6cba6 | |||
| 0a1aee591a | |||
| a0f73e6a32 | |||
| e8a8ce2528 | |||
| b2c3d3a301 | |||
| 759eb906b9 | |||
| 41870ee2e2 | |||
| 0ac2485bff | |||
| eb16990b0b | |||
| ce103137a5 | |||
| 547cc6e4ae | |||
| 32d23a7073 | |||
| 7890b18568 | |||
| 90ff7eb793 | |||
| d07f3ed716 | |||
| 7d2ae48067 | |||
| 1d877545ae | |||
| 87f3764ebf | |||
| 3ae4d639ec | |||
| a5550091d3 | |||
| 61402c183e | |||
| 64e4392a92 | |||
| ae73d2f87f | |||
| 86e36bc64a | |||
| d1817a3611 | |||
| d465934818 | |||
| c353d5bcef | |||
| bdda586ccd | |||
| 0d989dcb2c | |||
| 2f84f76d89 | |||
| 3343524325 | |||
| 28ecb4d786 | |||
| 338da0208c | |||
| ec67340e66 | |||
| 1c0286eda0 | |||
| 7d5ff47e8e | |||
| 5046171b76 | |||
| f374ae426c | |||
| 2a375b0a6f | |||
| a5d7f89fcb | |||
| 96ccf30e46 | |||
| 38dad49bbd | |||
| f2be7d0a53 | |||
| 9b4afb845b | |||
| f4b5f3960c | |||
| 127ea49d74 | |||
| 30e1fd23d6 | |||
| df517cfbfa | |||
| ec6ac443fb | |||
| 11781dd70b | |||
| b9ab27523f | |||
| 331543d74b | |||
| e7ef5c3d32 | |||
| 1a6870b8fe | |||
| ad6ac13d50 | |||
| c976d94bdd | |||
| 5d7dea5a84 | |||
| e5e3d38365 | |||
| 1af2b695dc | |||
| bbb1207b27 | |||
| 9cfa91e091 | |||
| 87101d6b04 | |||
| 1f254ca775 | |||
| d884a761ad | |||
| cfe2e4b7b4 | |||
| c93660ec4a | |||
| 0a0d43aa9f | |||
| bc0836fc3c | |||
| e7b493d7c9 | |||
| 185cd86efb | |||
| 5ee2db267a | |||
| 26a4746898 | |||
| 2e0ec73317 | |||
| b061959b18 | |||
| 982da89861 | |||
| 4ec7923164 | |||
| 4bc556aec5 | |||
| e520a34fa5 | |||
| d741a88299 | |||
| 36eca08895 | |||
| da9b559752 | |||
| 6677fe1e23 | |||
| b85259c013 | |||
| bb21d6a364 | |||
| ec89230893 | |||
| 2fc9c88cd5 | |||
| 3435b4714e | |||
| 7b6280b6dc | |||
| 140f3cbdba | |||
| ac084c4e79 | |||
| 71592e883a | |||
| e60b91fd45 | |||
| aae01c010b | |||
| 5e4cd8978d | |||
| e120812eae | |||
| 25b2dc6cec | |||
| df4536741d | |||
| e9161e8c93 | |||
| 4ed61380de | |||
| 98be276bd1 | |||
| 96d575feb7 |
114 changed files with 15325 additions and 1807 deletions
143
CHANGELOG.md
143
CHANGELOG.md
|
|
@ -5,6 +5,149 @@ 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/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.8.0 (2026-03-04)
|
||||
|
||||
### Feat
|
||||
|
||||
- improve support for exporting quantity, log data
|
||||
- show related Quantity records when viewing a Measure
|
||||
- show related Quantity records when viewing a Unit
|
||||
- show link to Log record when viewing Quantity
|
||||
|
||||
### Fix
|
||||
|
||||
- bump version requirement for wuttaweb
|
||||
|
||||
## v0.7.0 (2026-03-04)
|
||||
|
||||
### Feat
|
||||
|
||||
- expose "group membership" for assets
|
||||
- expose "current location" for assets
|
||||
- add schema, sync support for `Log.is_movement`
|
||||
- add schema, import support for `Asset.owners`
|
||||
- add schema, import support for `Log.quick`
|
||||
- show quantities when viewing log
|
||||
- add sync support for `MedicalLog.vet`
|
||||
- add schema, import support for `Log.quantities`
|
||||
- add schema, import support for `Log.groups`
|
||||
- add schema, import support for `Log.locations`
|
||||
- add sync support for `Log.is_group_assignment`
|
||||
- add support for exporting log status, timestamp to farmOS
|
||||
- add support for log 'owners'
|
||||
- add support for edit, import/export of plant type data
|
||||
- add way to create animal type when editing animal
|
||||
- add related version tables for asset/log revision history
|
||||
- improve mirror/deletion for assets, logs, animal types
|
||||
- auto-delete asset from farmOS if deleting via mirror app
|
||||
|
||||
### Fix
|
||||
|
||||
- show drupal ID column for asset types
|
||||
- remove unique constraint for `LandAsset.land_type_uuid`
|
||||
- move farmOS UUID field below the Drupal ID
|
||||
- add links for Parents column in All Assets grid
|
||||
- set timestamp for new log in quick eggs form
|
||||
- set default grid pagesize to 50
|
||||
- add placeholder for log 'quick' field
|
||||
- define log grid columns to match farmOS
|
||||
- make AllLogView inherit from LogMasterView
|
||||
- rename views for "all records" (all assets, all logs etc.)
|
||||
- ensure token refresh works regardless where API client is used
|
||||
- render links for Plant Type column in Plant Assets grid
|
||||
- fix land asset type
|
||||
- prevent edit for asset types, land types when app is mirror
|
||||
- add farmOS-style links for Parents column in Land Assets grid
|
||||
- remove unique constraint for `AnimalType.name`
|
||||
- prevent delete if animal type is still being referenced
|
||||
- add reminder to restart if changing integration mode
|
||||
- prevent edit for user farmos_uuid, drupal_id
|
||||
- remove 'contains' verb for sex filter
|
||||
- add enum, row hilite for log status
|
||||
- fix Sex field when empty and deleting an animal
|
||||
- add `get_farmos_client_for_user()` convenience function
|
||||
- use current user token for auto-sync within web app
|
||||
- set log type, status enums for log grids
|
||||
- add more default perms for first site admin user
|
||||
- only show quick form menu if perms allow
|
||||
- expose config for farmOS OAuth2 client_id and scope
|
||||
- add separate permission for each quick form view
|
||||
|
||||
## v0.6.0 (2026-02-25)
|
||||
|
||||
### Feat
|
||||
|
||||
- add common normalizer to simplify code in view, importer etc.
|
||||
- overhaul farmOS log views; add Eggs quick form
|
||||
- add basic CRUD for direct API views: animal types, animal assets
|
||||
- use 'include' API param for better Animal Assets grid data
|
||||
- add backend filters, sorting for farmOS animal types, assets
|
||||
- include/exclude certain views, menus based on integration mode
|
||||
- add Standard Quantities table, views, import
|
||||
- add Quantity Types table, views, import
|
||||
- add Units table, views, import/export
|
||||
|
||||
### Fix
|
||||
|
||||
- add `Notes` schema type
|
||||
- add grid filter for animal birthdate
|
||||
- add thumbnail to farmOS asset base view
|
||||
- add setting to toggle "farmOS-style grid links"
|
||||
- standardize a bit more for the farmOS Animal Assets view
|
||||
- set *default* instead of configured menu handler
|
||||
- expose farmOS integration mode, URL in app settings
|
||||
- reword some menu entries
|
||||
- add WuttaFarm -> farmOS export for Plant Assets
|
||||
- fix default admin user perms, per new log schema
|
||||
|
||||
## v0.5.0 (2026-02-18)
|
||||
|
||||
### Feat
|
||||
|
||||
- add `produces_eggs` flag for animal, group assets
|
||||
- add more assets (plant) and logs (harvest, medical, observation)
|
||||
- refactor log models, views to use generic/common base
|
||||
|
||||
### Fix
|
||||
|
||||
- rename db model modules, for better convention
|
||||
- add override for requests cert validation
|
||||
|
||||
## v0.4.1 (2026-02-17)
|
||||
|
||||
### Fix
|
||||
|
||||
- remove `AnimalType.changed` column
|
||||
|
||||
## v0.4.0 (2026-02-17)
|
||||
|
||||
### Feat
|
||||
|
||||
- add basic support for WuttaFarm → farmOS export
|
||||
- convert group assets to use common base/mixin
|
||||
- convert structure assets to use common base/mixin
|
||||
- convert land assets to use common base/mixin
|
||||
- add "generic" assets, new animal assets based on that
|
||||
|
||||
### Fix
|
||||
|
||||
- misc. field tweaks for asset forms
|
||||
- show warning when viewing an archived asset
|
||||
- fix some perms for all assets view
|
||||
- fix initial admin perms per route renaming
|
||||
- add parent relationships support for land assets
|
||||
- cleanup Land views to better match farmOS
|
||||
- cleanup Structure views to better match farmOS
|
||||
- cleanup Group views to better match farmOS
|
||||
- add / display thumbnail image for animals
|
||||
- improve handling of 'archived' records for grid/form views
|
||||
- use Male/Female dict enum for animal sex field
|
||||
- prevent direct edit of `farmos_uuid` and `drupal_id` fields
|
||||
- use same datetime display format as farmOS
|
||||
- convert `active` flag to `archived`
|
||||
- suppress output when user farmos/drupal keys are empty
|
||||
- customize page footer to mention farmOS
|
||||
|
||||
## v0.3.1 (2026-02-14)
|
||||
|
||||
### Fix
|
||||
|
|
|
|||
|
|
@ -18,8 +18,105 @@ Here is the list of features currently supported:
|
|||
* limited data is imported from farmOS API into native app tables
|
||||
* this data is exposed in views, similar to direct farmOS views (above)
|
||||
|
||||
* export some data back to farmOS
|
||||
* limited data is exported back via farmOS API, from native tables
|
||||
* supported tables are auto-synced when a record is created/updated
|
||||
* AnimalType
|
||||
* AnimalAsset
|
||||
* GroupAsset
|
||||
* LandAsset
|
||||
* StructureAsset
|
||||
|
||||
|
||||
How I Use This App
|
||||
------------------
|
||||
|
||||
My production farmOS instance is deployed via Podman container, which
|
||||
I prefer over Docker. (Not that I know much about any of that
|
||||
really.) It has a PostgreSQL database which runs in a separate
|
||||
container.
|
||||
|
||||
My production WuttaFarm instance is installed directly on the same
|
||||
host machine, in a Python virtual environment. PostgreSQL is also
|
||||
installed on the host machine; the app uses that for DB.
|
||||
|
||||
I ran the initial "special" import to establish the user accounts;
|
||||
then I ran the "full" import (farmOS → WuttaFarm). See also
|
||||
:doc:`/narr/install`.
|
||||
|
||||
I configured a cron job to run the full import every night, but in
|
||||
dry-run mode with warnings. This means I will get an email if
|
||||
WuttaFarm is ever out of sync with farmOS.
|
||||
|
||||
With all that in place, I can use WuttaFarm as my "daily driver" to
|
||||
add/edit assets (and soon, logs). Changes I make are immediately
|
||||
synced to farmOS, so as long as the overnight check does not send me
|
||||
an email, I know everything is good.
|
||||
|
||||
|
||||
Roadmap
|
||||
-------
|
||||
|
||||
Here are some things I still have planned so far:
|
||||
|
||||
* finish support for auto-sync, in current asset models
|
||||
* must make "asset parents" editable
|
||||
|
||||
* add more asset models?
|
||||
* i may only add those i need for now, but others can add more
|
||||
|
||||
* flesh out the log model support
|
||||
* add more tables, fields to schema
|
||||
* add/improve import and export
|
||||
* basically this should be as good as the asset model support
|
||||
* although again i may only add those i need for now
|
||||
|
||||
* add custom "quick forms" for assets and logs
|
||||
* again i probably will just add a few (e.g. egg collection)
|
||||
* but this could be an interesting path to go down, we'll see
|
||||
|
||||
* add custom "CSV/file importers"
|
||||
* the framework has some pretty neat tools around this, so..
|
||||
* ..even if i don't need CSV import i'd like to show what's possible
|
||||
|
||||
Notably **off the table** for now are:
|
||||
|
||||
* anything involving maps
|
||||
* file/image attachments
|
||||
|
||||
I will just import "thumbnail" and "large" image URLs from farmOS for
|
||||
each asset for now. Will have to think more on the image/attachment
|
||||
stuff before I'll know if/how to add support in WuttaFarm.
|
||||
|
||||
Maps will wait mostly because I have never done anything involving
|
||||
those (or GIS etc. - if that's even the right term). And anyway the
|
||||
main "use" for this app is probably around data entry, so it may never
|
||||
"need" maps support.
|
||||
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
.. image:: https://wuttaproject.org/images/screenshot.png
|
||||
|
||||
Login Screen
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://wuttaproject.org/images/wuttafarm/screenshot001.png
|
||||
|
||||
|
||||
List All Assets
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://wuttaproject.org/images/wuttafarm/screenshot002.png
|
||||
|
||||
|
||||
View Animal Asset
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://wuttaproject.org/images/wuttafarm/screenshot003.png
|
||||
|
||||
|
||||
Edit Animal Asset
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. image:: https://wuttaproject.org/images/wuttafarm/screenshot004.png
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttaFarm"
|
||||
version = "0.3.1"
|
||||
version = "0.8.0"
|
||||
description = "Web app to integrate with and extend farmOS"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
@ -34,7 +34,7 @@ dependencies = [
|
|||
"pyramid_exclog",
|
||||
"uvicorn[standard]",
|
||||
"WuttaSync",
|
||||
"WuttaWeb[continuum]>=0.27.4",
|
||||
"WuttaWeb[continuum]>=0.29.0",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -58,6 +58,7 @@ wuttafarm = "wuttafarm.app:WuttaFarmAppProvider"
|
|||
"wuttafarm" = "wuttafarm.web.menus:WuttaFarmMenuHandler"
|
||||
|
||||
[project.entry-points."wuttasync.importing"]
|
||||
"export.to_farmos.from_wuttafarm" = "wuttafarm.farmos.importing.wuttafarm:FromWuttaFarmToFarmOS"
|
||||
"import.to_wuttafarm.from_farmos" = "wuttafarm.importing.farmos:FromFarmOSToWuttaFarm"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,26 @@ class WuttaFarmAppHandler(base.AppHandler):
|
|||
Custom :term:`app handler` for WuttaFarm.
|
||||
"""
|
||||
|
||||
display_format_datetime = "%a, %m/%d/%Y - %H:%M"
|
||||
|
||||
default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
|
||||
default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler"
|
||||
|
||||
def get_asset_handler(self):
|
||||
"""
|
||||
Get the configured asset handler.
|
||||
|
||||
:rtype: :class:`~wuttafarm.assets.AssetHandler`
|
||||
"""
|
||||
if "asset" not in self.handlers:
|
||||
spec = self.config.get(
|
||||
f"{self.appname}.asset_handler",
|
||||
default="wuttafarm.assets:AssetHandler",
|
||||
)
|
||||
factory = self.load_object(spec)
|
||||
self.handlers["asset"] = factory(self.config)
|
||||
return self.handlers["asset"]
|
||||
|
||||
def get_farmos_handler(self):
|
||||
"""
|
||||
Get the configured farmOS integration handler.
|
||||
|
|
@ -49,6 +66,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
|
||||
|
|
@ -83,6 +138,85 @@ class WuttaFarmAppHandler(base.AppHandler):
|
|||
handler = self.get_farmos_handler()
|
||||
return handler.is_farmos_4x(*args, **kwargs)
|
||||
|
||||
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, client=None, require=True):
|
||||
"""
|
||||
Export the given object to farmOS, using configured handler.
|
||||
|
||||
This should ensure the given object is also *updated* with the
|
||||
farmOS UUID and Drupal ID, when new record is created in
|
||||
farmOS.
|
||||
|
||||
:param obj: Any data object in WuttaFarm, e.g. AnimalAsset
|
||||
instance.
|
||||
|
||||
:param client: Existing farmOS API client to use. If not
|
||||
specified, a new one will be instantiated.
|
||||
|
||||
:param require: If true, this will *require* the export
|
||||
handler to support objects of the given type. If false,
|
||||
then nothing will happen / export is silently skipped when
|
||||
there is no such exporter.
|
||||
"""
|
||||
handler = self.app.get_import_handler("export.to_farmos.from_wuttafarm")
|
||||
|
||||
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}")
|
||||
return
|
||||
|
||||
# nb. begin txn to establish the API client
|
||||
handler.begin_target_transaction(client)
|
||||
importer = handler.get_importer(model_name, caches_target=False)
|
||||
normal = importer.normalize_source_object(obj)
|
||||
importer.process_data(source_data=[normal])
|
||||
|
||||
def auto_sync_from_farmos(self, obj, model_name, client=None, 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 client: Existing farmOS API client to use. If not
|
||||
specified, a new one will be instantiated.
|
||||
|
||||
: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
|
||||
handler.begin_source_transaction(client)
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
65
src/wuttafarm/assets.py
Normal file
65
src/wuttafarm/assets.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Asset handler
|
||||
"""
|
||||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
|
||||
|
||||
class AssetHandler(GenericHandler):
|
||||
"""
|
||||
Base class and default implementation for the asset
|
||||
:term:`handler`.
|
||||
"""
|
||||
|
||||
def get_groups(self, asset):
|
||||
model = self.app.model
|
||||
session = self.app.get_session(asset)
|
||||
|
||||
grplog = (
|
||||
session.query(model.Log)
|
||||
.join(model.LogAsset)
|
||||
.filter(model.LogAsset.asset == asset)
|
||||
.filter(model.Log.is_group_assignment == True)
|
||||
.order_by(model.Log.timestamp.desc())
|
||||
.first()
|
||||
)
|
||||
if grplog:
|
||||
return grplog.groups
|
||||
return []
|
||||
|
||||
def get_locations(self, asset):
|
||||
model = self.app.model
|
||||
session = self.app.get_session(asset)
|
||||
|
||||
loclog = (
|
||||
session.query(model.Log)
|
||||
.join(model.LogAsset)
|
||||
.filter(model.LogAsset.asset == asset)
|
||||
.filter(model.Log.is_movement == True)
|
||||
.order_by(model.Log.timestamp.desc())
|
||||
.first()
|
||||
)
|
||||
if loclog:
|
||||
return loclog.locations
|
||||
return []
|
||||
|
|
@ -26,5 +26,6 @@ WuttaFarm CLI
|
|||
from .base import wuttafarm_typer
|
||||
|
||||
# nb. must bring in all modules for discovery to work
|
||||
from . import export_farmos
|
||||
from . import import_farmos
|
||||
from . import install
|
||||
|
|
|
|||
41
src/wuttafarm/cli/export_farmos.py
Normal file
41
src/wuttafarm/cli/export_farmos.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
See also: :ref:`wuttafarm-export-farmos`
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from wuttasync.cli import import_command, ImportCommandHandler
|
||||
|
||||
from wuttafarm.cli import wuttafarm_typer
|
||||
|
||||
|
||||
@wuttafarm_typer.command()
|
||||
@import_command
|
||||
def export_farmos(ctx: typer.Context, **kwargs):
|
||||
"""
|
||||
Export data from WuttaFarm to farmOS API
|
||||
"""
|
||||
config = ctx.parent.wutta_config
|
||||
handler = ImportCommandHandler(config, key="export.to_farmos.from_wuttafarm")
|
||||
handler.run(ctx)
|
||||
|
|
@ -23,6 +23,8 @@
|
|||
WuttaFarm config extensions
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from wuttjamaican.conf import WuttaConfigExtension
|
||||
|
||||
|
||||
|
|
@ -39,19 +41,26 @@ class WuttaFarmConfig(WuttaConfigExtension):
|
|||
config.setdefault(f"{config.appname}.app_title", "WuttaFarm")
|
||||
config.setdefault(f"{config.appname}.app_dist", "WuttaFarm")
|
||||
|
||||
# app model
|
||||
# app model/enum
|
||||
config.setdefault(f"{config.appname}.model_spec", "wuttafarm.db.model")
|
||||
config.setdefault(f"{config.appname}.enum_spec", "wuttafarm.enum")
|
||||
|
||||
# app handler
|
||||
config.setdefault(
|
||||
f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler"
|
||||
)
|
||||
|
||||
# web app menu
|
||||
# web app stuff
|
||||
config.setdefault(
|
||||
f"{config.appname}.web.menus.handler.spec",
|
||||
f"{config.appname}.web.menus.handler.default_spec",
|
||||
"wuttafarm.web.menus:WuttaFarmMenuHandler",
|
||||
)
|
||||
config.setdefault("wuttaweb.grids.default_pagesize", "50")
|
||||
|
||||
# web app libcache
|
||||
# config.setdefault('wuttaweb.static_libcache.module', 'wuttafarm.web.static')
|
||||
|
||||
# maybe override cert validation for requests lib.
|
||||
# nb. this is "global" and not "specific" to the farmos API requests!
|
||||
if bundle := config.get(f"{config.appname}.requests_ca_bundle"):
|
||||
os.environ.setdefault("REQUESTS_CA_BUNDLE", bundle)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
"""add Log.is_movement
|
||||
|
||||
Revision ID: 0771322957bd
|
||||
Revises: 12de43facb95
|
||||
Create Date: 2026-03-02 20:21:03.889847
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "0771322957bd"
|
||||
down_revision: Union[str, None] = "12de43facb95"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log
|
||||
op.add_column("log", sa.Column("is_movement", sa.Boolean(), nullable=True))
|
||||
op.add_column(
|
||||
"log_version",
|
||||
sa.Column("is_movement", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log
|
||||
op.drop_column("log_version", "is_movement")
|
||||
op.drop_column("log", "is_movement")
|
||||
|
|
@ -0,0 +1,596 @@
|
|||
"""add Plant Assets and more Logs
|
||||
|
||||
Revision ID: 11e0e46f48a6
|
||||
Revises: dd6351e69233
|
||||
Create Date: 2026-02-18 18:11:46.536930
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "11e0e46f48a6"
|
||||
down_revision: Union[str, None] = "dd6351e69233"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# plant_type
|
||||
op.create_table(
|
||||
"plant_type",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.String(length=255), nullable=True),
|
||||
sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||
sa.Column("drupal_id", sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_plant_type")),
|
||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_plant_type_drupal_id")),
|
||||
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_plant_type_farmos_uuid")),
|
||||
sa.UniqueConstraint("name", name=op.f("uq_plant_type_name")),
|
||||
)
|
||||
op.create_table(
|
||||
"plant_type_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("name", sa.String(length=100), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"description", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"farmos_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("drupal_id", sa.Integer(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_plant_type_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_plant_type_version_end_transaction_id"),
|
||||
"plant_type_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_plant_type_version_operation_type"),
|
||||
"plant_type_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_plant_type_version_pk_transaction_id",
|
||||
"plant_type_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_plant_type_version_pk_validity",
|
||||
"plant_type_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_plant_type_version_transaction_id"),
|
||||
"plant_type_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_plant
|
||||
op.create_table(
|
||||
"asset_plant",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["asset.uuid"], name=op.f("fk_asset_plant_uuid_asset")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_plant")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_plant_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_plant_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_plant_version_end_transaction_id"),
|
||||
"asset_plant_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_plant_version_operation_type"),
|
||||
"asset_plant_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_plant_version_pk_transaction_id",
|
||||
"asset_plant_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_plant_version_pk_validity",
|
||||
"asset_plant_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_plant_version_transaction_id"),
|
||||
"asset_plant_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_plant_plant_type
|
||||
op.create_table(
|
||||
"asset_plant_plant_type",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("plant_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("plant_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["plant_asset_uuid"],
|
||||
["asset_plant.uuid"],
|
||||
name=op.f("fk_asset_plant_plant_type_plant_asset_uuid_asset_plant"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["plant_type_uuid"],
|
||||
["plant_type.uuid"],
|
||||
name=op.f("fk_asset_plant_plant_type_plant_type_uuid_plant_type"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_plant_plant_type")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_plant_plant_type_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"plant_asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"plant_type_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_plant_plant_type_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_plant_plant_type_version_end_transaction_id"),
|
||||
"asset_plant_plant_type_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_plant_plant_type_version_operation_type"),
|
||||
"asset_plant_plant_type_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_plant_plant_type_version_pk_transaction_id",
|
||||
"asset_plant_plant_type_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_plant_plant_type_version_pk_validity",
|
||||
"asset_plant_plant_type_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_plant_plant_type_version_transaction_id"),
|
||||
"asset_plant_plant_type_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# log_asset
|
||||
op.create_table(
|
||||
"log_asset",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["asset_uuid"], ["asset.uuid"], name=op.f("fk_log_asset_asset_uuid_asset")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["log_uuid"], ["log.uuid"], name=op.f("fk_log_asset_log_uuid_log")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_asset")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_asset_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_log_asset_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_asset_version_end_transaction_id"),
|
||||
"log_asset_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_asset_version_operation_type"),
|
||||
"log_asset_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_asset_version_pk_transaction_id",
|
||||
"log_asset_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_asset_version_pk_validity",
|
||||
"log_asset_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_asset_version_transaction_id"),
|
||||
"log_asset_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# log_harvest
|
||||
op.create_table(
|
||||
"log_harvest",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["log.uuid"], name=op.f("fk_log_harvest_uuid_log")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_harvest")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_harvest_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_log_harvest_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_harvest_version_end_transaction_id"),
|
||||
"log_harvest_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_harvest_version_operation_type"),
|
||||
"log_harvest_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_harvest_version_pk_transaction_id",
|
||||
"log_harvest_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_harvest_version_pk_validity",
|
||||
"log_harvest_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_harvest_version_transaction_id"),
|
||||
"log_harvest_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# log_medical
|
||||
op.create_table(
|
||||
"log_medical",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["log.uuid"], name=op.f("fk_log_medical_uuid_log")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_medical")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_medical_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_log_medical_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_medical_version_end_transaction_id"),
|
||||
"log_medical_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_medical_version_operation_type"),
|
||||
"log_medical_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_medical_version_pk_transaction_id",
|
||||
"log_medical_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_medical_version_pk_validity",
|
||||
"log_medical_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_medical_version_transaction_id"),
|
||||
"log_medical_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# log_observation
|
||||
op.create_table(
|
||||
"log_observation",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["log.uuid"], name=op.f("fk_log_observation_uuid_log")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_observation")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_observation_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_log_observation_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_observation_version_end_transaction_id"),
|
||||
"log_observation_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_observation_version_operation_type"),
|
||||
"log_observation_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_observation_version_pk_transaction_id",
|
||||
"log_observation_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_observation_version_pk_validity",
|
||||
"log_observation_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_observation_version_transaction_id"),
|
||||
"log_observation_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_observation
|
||||
op.drop_index(
|
||||
op.f("ix_log_observation_version_transaction_id"),
|
||||
table_name="log_observation_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_observation_version_pk_validity", table_name="log_observation_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_observation_version_pk_transaction_id",
|
||||
table_name="log_observation_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_observation_version_operation_type"),
|
||||
table_name="log_observation_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_observation_version_end_transaction_id"),
|
||||
table_name="log_observation_version",
|
||||
)
|
||||
op.drop_table("log_observation_version")
|
||||
op.drop_table("log_observation")
|
||||
|
||||
# log_medical
|
||||
op.drop_index(
|
||||
op.f("ix_log_medical_version_transaction_id"), table_name="log_medical_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_medical_version_pk_validity", table_name="log_medical_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_medical_version_pk_transaction_id", table_name="log_medical_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_medical_version_operation_type"), table_name="log_medical_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_medical_version_end_transaction_id"),
|
||||
table_name="log_medical_version",
|
||||
)
|
||||
op.drop_table("log_medical_version")
|
||||
op.drop_table("log_medical")
|
||||
|
||||
# log_harvest
|
||||
op.drop_index(
|
||||
op.f("ix_log_harvest_version_transaction_id"), table_name="log_harvest_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_harvest_version_pk_validity", table_name="log_harvest_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_harvest_version_pk_transaction_id", table_name="log_harvest_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_harvest_version_operation_type"), table_name="log_harvest_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_harvest_version_end_transaction_id"),
|
||||
table_name="log_harvest_version",
|
||||
)
|
||||
op.drop_table("log_harvest_version")
|
||||
op.drop_table("log_harvest")
|
||||
|
||||
# log_asset
|
||||
op.drop_index(
|
||||
op.f("ix_log_asset_version_transaction_id"), table_name="log_asset_version"
|
||||
)
|
||||
op.drop_index("ix_log_asset_version_pk_validity", table_name="log_asset_version")
|
||||
op.drop_index(
|
||||
"ix_log_asset_version_pk_transaction_id", table_name="log_asset_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_asset_version_operation_type"), table_name="log_asset_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_asset_version_end_transaction_id"), table_name="log_asset_version"
|
||||
)
|
||||
op.drop_table("log_asset_version")
|
||||
op.drop_table("log_asset")
|
||||
|
||||
# asset_plant_plant_type
|
||||
op.drop_index(
|
||||
op.f("ix_asset_plant_plant_type_version_transaction_id"),
|
||||
table_name="asset_plant_plant_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_plant_plant_type_version_pk_validity",
|
||||
table_name="asset_plant_plant_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_plant_plant_type_version_pk_transaction_id",
|
||||
table_name="asset_plant_plant_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_plant_plant_type_version_operation_type"),
|
||||
table_name="asset_plant_plant_type_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_plant_plant_type_version_end_transaction_id"),
|
||||
table_name="asset_plant_plant_type_version",
|
||||
)
|
||||
op.drop_table("asset_plant_plant_type_version")
|
||||
op.drop_table("asset_plant_plant_type")
|
||||
|
||||
# asset_plant
|
||||
op.drop_index(
|
||||
op.f("ix_asset_plant_version_transaction_id"), table_name="asset_plant_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_plant_version_pk_validity", table_name="asset_plant_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_plant_version_pk_transaction_id", table_name="asset_plant_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_plant_version_operation_type"), table_name="asset_plant_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_plant_version_end_transaction_id"),
|
||||
table_name="asset_plant_version",
|
||||
)
|
||||
op.drop_table("asset_plant_version")
|
||||
op.drop_table("asset_plant")
|
||||
|
||||
# plant_type
|
||||
op.drop_index(
|
||||
op.f("ix_plant_type_version_transaction_id"), table_name="plant_type_version"
|
||||
)
|
||||
op.drop_index("ix_plant_type_version_pk_validity", table_name="plant_type_version")
|
||||
op.drop_index(
|
||||
"ix_plant_type_version_pk_transaction_id", table_name="plant_type_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_plant_type_version_operation_type"), table_name="plant_type_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_plant_type_version_end_transaction_id"),
|
||||
table_name="plant_type_version",
|
||||
)
|
||||
op.drop_table("plant_type_version")
|
||||
op.drop_table("plant_type")
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
"""add Asset.owners
|
||||
|
||||
Revision ID: 12de43facb95
|
||||
Revises: 85d4851e8292
|
||||
Create Date: 2026-03-02 19:03:35.511398
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "12de43facb95"
|
||||
down_revision: Union[str, None] = "85d4851e8292"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_owner
|
||||
op.create_table(
|
||||
"asset_owner",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("user_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["asset_uuid"], ["asset.uuid"], name=op.f("fk_asset_owner_asset_uuid_asset")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_uuid"], ["user.uuid"], name=op.f("fk_asset_owner_user_uuid_user")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_owner")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_owner_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"user_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_owner_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_owner_version_end_transaction_id"),
|
||||
"asset_owner_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_owner_version_operation_type"),
|
||||
"asset_owner_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_owner_version_pk_transaction_id",
|
||||
"asset_owner_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_owner_version_pk_validity",
|
||||
"asset_owner_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_owner_version_transaction_id"),
|
||||
"asset_owner_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# asset_owner
|
||||
op.drop_index(
|
||||
op.f("ix_asset_owner_version_transaction_id"), table_name="asset_owner_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_owner_version_pk_validity", table_name="asset_owner_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_owner_version_pk_transaction_id", table_name="asset_owner_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_owner_version_operation_type"), table_name="asset_owner_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_owner_version_end_transaction_id"),
|
||||
table_name="asset_owner_version",
|
||||
)
|
||||
op.drop_table("asset_owner_version")
|
||||
op.drop_table("asset_owner")
|
||||
|
|
@ -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,41 @@
|
|||
"""add animal thumbnail url
|
||||
|
||||
Revision ID: 2a49127e974b
|
||||
Revises: 8898184c5c75
|
||||
Create Date: 2026-02-14 19:41:22.039343
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "2a49127e974b"
|
||||
down_revision: Union[str, None] = "8898184c5c75"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# animal
|
||||
op.add_column(
|
||||
"animal", sa.Column("thumbnail_url", sa.String(length=255), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"animal_version",
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# animal
|
||||
op.drop_column("animal_version", "thumbnail_url")
|
||||
op.drop_column("animal", "thumbnail_url")
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
"""use shared base for Structure Assets
|
||||
|
||||
Revision ID: 34ec51d80f52
|
||||
Revises: d882682c82f9
|
||||
Create Date: 2026-02-15 13:19:18.814523
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "34ec51d80f52"
|
||||
down_revision: Union[str, None] = "d882682c82f9"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_structure
|
||||
op.create_table(
|
||||
"asset_structure",
|
||||
sa.Column("structure_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["structure_type_uuid"],
|
||||
["structure_type.uuid"],
|
||||
name=op.f("fk_asset_structure_structure_type_uuid_structure_type"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["asset.uuid"], name=op.f("fk_asset_structure_uuid_asset")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_structure")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_structure_version",
|
||||
sa.Column(
|
||||
"structure_type_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_structure_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_structure_version_end_transaction_id"),
|
||||
"asset_structure_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_structure_version_operation_type"),
|
||||
"asset_structure_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_structure_version_pk_transaction_id",
|
||||
"asset_structure_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_structure_version_pk_validity",
|
||||
"asset_structure_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_structure_version_transaction_id"),
|
||||
"asset_structure_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# structure
|
||||
op.drop_index(
|
||||
op.f("ix_structure_version_end_transaction_id"), table_name="structure_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_structure_version_operation_type"), table_name="structure_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_structure_version_pk_transaction_id"), table_name="structure_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_structure_version_pk_validity"), table_name="structure_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_structure_version_transaction_id"), table_name="structure_version"
|
||||
)
|
||||
op.drop_table("structure_version")
|
||||
op.drop_table("structure")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# structure
|
||||
op.create_table(
|
||||
"structure",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"structure_type_uuid", sa.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["structure_type_uuid"],
|
||||
["structure_type.uuid"],
|
||||
name=op.f("fk_structure_structure_type_uuid_structure_type"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_structure")),
|
||||
sa.UniqueConstraint(
|
||||
"drupal_id",
|
||||
name=op.f("uq_structure_drupal_id"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"farmos_uuid",
|
||||
name=op.f("uq_structure_farmos_uuid"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"name",
|
||||
name=op.f("uq_structure_name"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
)
|
||||
op.create_table(
|
||||
"structure_version",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("structure_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_structure_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_structure_version_transaction_id"),
|
||||
"structure_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_structure_version_pk_validity"),
|
||||
"structure_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_structure_version_pk_transaction_id"),
|
||||
"structure_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_structure_version_operation_type"),
|
||||
"structure_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_structure_version_end_transaction_id"),
|
||||
"structure_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_structure
|
||||
op.drop_index(
|
||||
op.f("ix_asset_structure_version_transaction_id"),
|
||||
table_name="asset_structure_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_structure_version_pk_validity", table_name="asset_structure_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_structure_version_pk_transaction_id",
|
||||
table_name="asset_structure_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_structure_version_operation_type"),
|
||||
table_name="asset_structure_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_structure_version_end_transaction_id"),
|
||||
table_name="asset_structure_version",
|
||||
)
|
||||
op.drop_table("asset_structure_version")
|
||||
op.drop_table("asset_structure")
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
"""add LogLocation
|
||||
|
||||
Revision ID: 3bef7d380a38
|
||||
Revises: f3c7e273bfa3
|
||||
Create Date: 2026-02-28 20:41:56.051847
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "3bef7d380a38"
|
||||
down_revision: Union[str, None] = "f3c7e273bfa3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log_location
|
||||
op.create_table(
|
||||
"log_location",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["asset_uuid"],
|
||||
["asset.uuid"],
|
||||
name=op.f("fk_log_location_asset_uuid_asset"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["log_uuid"], ["log.uuid"], name=op.f("fk_log_location_log_uuid_log")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_location")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_location_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_log_location_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_location_version_end_transaction_id"),
|
||||
"log_location_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_location_version_operation_type"),
|
||||
"log_location_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_location_version_pk_transaction_id",
|
||||
"log_location_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_location_version_pk_validity",
|
||||
"log_location_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_location_version_transaction_id"),
|
||||
"log_location_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_location
|
||||
op.drop_index(
|
||||
op.f("ix_log_location_version_transaction_id"),
|
||||
table_name="log_location_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_location_version_pk_validity", table_name="log_location_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_location_version_pk_transaction_id", table_name="log_location_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_location_version_operation_type"),
|
||||
table_name="log_location_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_location_version_end_transaction_id"),
|
||||
table_name="log_location_version",
|
||||
)
|
||||
op.drop_table("log_location_version")
|
||||
op.drop_table("log_location")
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"""remove unique for animal_type.name
|
||||
|
||||
Revision ID: 45c7718d2ed2
|
||||
Revises: 5b6c87d8cddf
|
||||
Create Date: 2026-02-27 16:53:59.310342
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "45c7718d2ed2"
|
||||
down_revision: Union[str, None] = "5b6c87d8cddf"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# animal_type
|
||||
op.drop_constraint(op.f("uq_animal_type_name"), "animal_type", type_="unique")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# animal_type
|
||||
op.create_unique_constraint(
|
||||
op.f("uq_animal_type_name"),
|
||||
"animal_type",
|
||||
["name"],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
)
|
||||
108
src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py
Normal file
108
src/wuttafarm/db/alembic/versions/47d0ebd84554_add_logowner.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""add LogOwner
|
||||
|
||||
Revision ID: 47d0ebd84554
|
||||
Revises: 45c7718d2ed2
|
||||
Create Date: 2026-02-28 19:18:49.122090
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "47d0ebd84554"
|
||||
down_revision: Union[str, None] = "45c7718d2ed2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log_owner
|
||||
op.create_table(
|
||||
"log_owner",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("user_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["log_uuid"], ["log.uuid"], name=op.f("fk_log_owner_log_uuid_log")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_uuid"], ["user.uuid"], name=op.f("fk_log_owner_user_uuid_user")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_owner")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_owner_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"user_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_log_owner_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_owner_version_end_transaction_id"),
|
||||
"log_owner_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_owner_version_operation_type"),
|
||||
"log_owner_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_owner_version_pk_transaction_id",
|
||||
"log_owner_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_owner_version_pk_validity",
|
||||
"log_owner_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_owner_version_transaction_id"),
|
||||
"log_owner_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_owner
|
||||
op.drop_index(
|
||||
op.f("ix_log_owner_version_transaction_id"), table_name="log_owner_version"
|
||||
)
|
||||
op.drop_index("ix_log_owner_version_pk_validity", table_name="log_owner_version")
|
||||
op.drop_index(
|
||||
"ix_log_owner_version_pk_transaction_id", table_name="log_owner_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_owner_version_operation_type"), table_name="log_owner_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_owner_version_end_transaction_id"), table_name="log_owner_version"
|
||||
)
|
||||
op.drop_table("log_owner_version")
|
||||
op.drop_table("log_owner")
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
"""add LandAssetParent model
|
||||
|
||||
Revision ID: 554e6168c339
|
||||
Revises: 8cc1565d38e7
|
||||
Create Date: 2026-02-14 20:41:24.859064
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "554e6168c339"
|
||||
down_revision: Union[str, None] = "8cc1565d38e7"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# land_asset_parent
|
||||
op.create_table(
|
||||
"land_asset_parent",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("land_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("parent_asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["land_asset_uuid"],
|
||||
["land_asset.uuid"],
|
||||
name=op.f("fk_land_asset_parent_land_asset_uuid_land_asset"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_asset_uuid"],
|
||||
["land_asset.uuid"],
|
||||
name=op.f("fk_land_asset_parent_parent_asset_uuid_land_asset"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset_parent")),
|
||||
)
|
||||
op.create_table(
|
||||
"land_asset_parent_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"land_asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"parent_asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_land_asset_parent_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_end_transaction_id"),
|
||||
"land_asset_parent_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_operation_type"),
|
||||
"land_asset_parent_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_land_asset_parent_version_pk_transaction_id",
|
||||
"land_asset_parent_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_land_asset_parent_version_pk_validity",
|
||||
"land_asset_parent_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_transaction_id"),
|
||||
"land_asset_parent_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# land_asset_parent
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_transaction_id"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_land_asset_parent_version_pk_validity",
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_land_asset_parent_version_pk_transaction_id",
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_operation_type"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_end_transaction_id"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_table("land_asset_parent_version")
|
||||
op.drop_table("land_asset_parent")
|
||||
|
|
@ -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")
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
"""remove unwanted unique constraint
|
||||
|
||||
Revision ID: 5f474125a80e
|
||||
Revises: 0771322957bd
|
||||
Create Date: 2026-03-04 12:03:16.034291
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "5f474125a80e"
|
||||
down_revision: Union[str, None] = "0771322957bd"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_land
|
||||
op.drop_constraint(
|
||||
op.f("uq_asset_land_land_type_uuid"), "asset_land", type_="unique"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# asset_land
|
||||
op.create_unique_constraint(
|
||||
op.f("uq_asset_land_land_type_uuid"),
|
||||
"asset_land",
|
||||
["land_type_uuid"],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
)
|
||||
111
src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py
Normal file
111
src/wuttafarm/db/alembic/versions/74d32b4ec210_add_loggroup.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""add LogGroup
|
||||
|
||||
Revision ID: 74d32b4ec210
|
||||
Revises: 3bef7d380a38
|
||||
Create Date: 2026-02-28 21:35:24.125784
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "74d32b4ec210"
|
||||
down_revision: Union[str, None] = "3bef7d380a38"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log_group
|
||||
op.create_table(
|
||||
"log_group",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["asset_uuid"], ["asset.uuid"], name=op.f("fk_log_group_asset_uuid_asset")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["log_uuid"], ["log.uuid"], name=op.f("fk_log_group_log_uuid_log")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_group")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_group_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_log_group_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_group_version_end_transaction_id"),
|
||||
"log_group_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_group_version_operation_type"),
|
||||
"log_group_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_group_version_pk_transaction_id",
|
||||
"log_group_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_group_version_pk_validity",
|
||||
"log_group_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_group_version_transaction_id"),
|
||||
"log_group_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_group
|
||||
op.drop_index(
|
||||
op.f("ix_log_group_version_transaction_id"), table_name="log_group_version"
|
||||
)
|
||||
op.drop_index("ix_log_group_version_pk_validity", table_name="log_group_version")
|
||||
op.drop_index(
|
||||
"ix_log_group_version_pk_transaction_id", table_name="log_group_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_group_version_operation_type"), table_name="log_group_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_group_version_end_transaction_id"), table_name="log_group_version"
|
||||
)
|
||||
op.drop_table("log_group_version")
|
||||
op.drop_table("log_group")
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
"""add produces_eggs via EggMixin
|
||||
|
||||
Revision ID: 82a03f4ef1a4
|
||||
Revises: 11e0e46f48a6
|
||||
Create Date: 2026-02-18 18:45:36.015144
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "82a03f4ef1a4"
|
||||
down_revision: Union[str, None] = "11e0e46f48a6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_animal
|
||||
op.add_column(
|
||||
"asset_animal", sa.Column("produces_eggs", sa.Boolean(), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"asset_animal_version",
|
||||
sa.Column("produces_eggs", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
)
|
||||
|
||||
# asset_group
|
||||
op.add_column(
|
||||
"asset_group", sa.Column("produces_eggs", sa.Boolean(), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"asset_group_version",
|
||||
sa.Column("produces_eggs", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# asset_group
|
||||
op.drop_column("asset_group_version", "produces_eggs")
|
||||
op.drop_column("asset_group", "produces_eggs")
|
||||
|
||||
# asset_animal
|
||||
op.drop_column("asset_animal_version", "produces_eggs")
|
||||
op.drop_column("asset_animal", "produces_eggs")
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"""add Log.quick
|
||||
|
||||
Revision ID: 85d4851e8292
|
||||
Revises: d459db991404
|
||||
Create Date: 2026-03-02 18:42:56.070281
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "85d4851e8292"
|
||||
down_revision: Union[str, None] = "d459db991404"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log
|
||||
op.add_column("log", sa.Column("quick", sa.String(length=20), nullable=True))
|
||||
op.add_column(
|
||||
"log_version",
|
||||
sa.Column("quick", sa.String(length=20), autoincrement=False, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log
|
||||
op.drop_column("log_version", "quick")
|
||||
op.drop_column("log", "quick")
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
"""convert active to archived
|
||||
|
||||
Revision ID: 8898184c5c75
|
||||
Revises: 3e2ef02bf264
|
||||
Create Date: 2026-02-14 18:41:23.042951
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "8898184c5c75"
|
||||
down_revision: Union[str, None] = "3e2ef02bf264"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# animal
|
||||
op.alter_column("animal", "active", new_column_name="archived")
|
||||
animal = sa.sql.table(
|
||||
"animal",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(animal.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
animal.update()
|
||||
.where(animal.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
op.alter_column("animal_version", "active", new_column_name="archived")
|
||||
animal_version = sa.sql.table(
|
||||
"animal_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(animal_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
animal_version.update()
|
||||
.where(animal_version.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
|
||||
# group
|
||||
op.alter_column("group", "active", new_column_name="archived")
|
||||
group = sa.sql.table(
|
||||
"group",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(group.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
group.update()
|
||||
.where(group.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
op.alter_column("group_version", "active", new_column_name="archived")
|
||||
group_version = sa.sql.table(
|
||||
"group_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(group_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
group_version.update()
|
||||
.where(group_version.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
|
||||
# land_asset
|
||||
op.alter_column("land_asset", "active", new_column_name="archived")
|
||||
land_asset = sa.sql.table(
|
||||
"land_asset",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(land_asset.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
land_asset.update()
|
||||
.where(land_asset.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
op.alter_column("land_asset_version", "active", new_column_name="archived")
|
||||
land_asset_version = sa.sql.table(
|
||||
"land_asset_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(land_asset_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
land_asset_version.update()
|
||||
.where(land_asset_version.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
|
||||
# structure
|
||||
op.alter_column("structure", "active", new_column_name="archived")
|
||||
structure = sa.sql.table(
|
||||
"structure",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(structure.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
structure.update()
|
||||
.where(structure.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
op.alter_column("structure_version", "active", new_column_name="archived")
|
||||
structure_version = sa.sql.table(
|
||||
"structure_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("archived"),
|
||||
)
|
||||
cursor = op.get_bind().execute(structure_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
structure_version.update()
|
||||
.where(structure_version.c.uuid == row.uuid)
|
||||
.values({"archived": not row.archived})
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# structure
|
||||
op.alter_column("structure", "archived", new_column_name="active")
|
||||
structure = sa.sql.table(
|
||||
"structure",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(structure.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
structure.update()
|
||||
.where(structure.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
op.alter_column("structure_version", "archived", new_column_name="active")
|
||||
structure_version = sa.sql.table(
|
||||
"structure_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(structure_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
structure_version.update()
|
||||
.where(structure_version.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
|
||||
# land_asset
|
||||
op.alter_column("land_asset", "archived", new_column_name="active")
|
||||
land_asset = sa.sql.table(
|
||||
"land_asset",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(land_asset.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
land_asset.update()
|
||||
.where(land_asset.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
op.alter_column("land_asset_version", "archived", new_column_name="active")
|
||||
land_asset_version = sa.sql.table(
|
||||
"land_asset_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(land_asset_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
land_asset_version.update()
|
||||
.where(land_asset_version.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
|
||||
# group
|
||||
op.alter_column("group", "archived", new_column_name="active")
|
||||
group = sa.sql.table(
|
||||
"group",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(group.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
group.update()
|
||||
.where(group.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
op.alter_column("group_version", "archived", new_column_name="active")
|
||||
group_version = sa.sql.table(
|
||||
"group_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(group_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
group_version.update()
|
||||
.where(group_version.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
|
||||
# animal
|
||||
op.alter_column("animal", "archived", new_column_name="active")
|
||||
animal = sa.sql.table(
|
||||
"animal",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(animal.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
animal.update()
|
||||
.where(animal.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
op.alter_column("animal_version", "archived", new_column_name="active")
|
||||
animal_version = sa.sql.table(
|
||||
"animal_version",
|
||||
sa.sql.column("uuid"),
|
||||
sa.sql.column("active"),
|
||||
)
|
||||
cursor = op.get_bind().execute(animal_version.select())
|
||||
for row in cursor.fetchall():
|
||||
op.get_bind().execute(
|
||||
animal_version.update()
|
||||
.where(animal_version.c.uuid == row.uuid)
|
||||
.values({"active": not row.active})
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
"""add structure thumbnail url
|
||||
|
||||
Revision ID: 8cc1565d38e7
|
||||
Revises: 2a49127e974b
|
||||
Create Date: 2026-02-14 20:07:33.913573
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "8cc1565d38e7"
|
||||
down_revision: Union[str, None] = "2a49127e974b"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# structure
|
||||
op.add_column(
|
||||
"structure", sa.Column("thumbnail_url", sa.String(length=255), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"structure_version",
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# structure
|
||||
op.drop_column("structure_version", "thumbnail_url")
|
||||
op.drop_column("structure", "thumbnail_url")
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
"""add LogQuantity
|
||||
|
||||
Revision ID: 9e875e5cbdc1
|
||||
Revises: 74d32b4ec210
|
||||
Create Date: 2026-02-28 21:55:31.876087
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "9e875e5cbdc1"
|
||||
down_revision: Union[str, None] = "74d32b4ec210"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log_quantity
|
||||
op.create_table(
|
||||
"log_quantity",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("log_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("quantity_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["log_uuid"], ["log.uuid"], name=op.f("fk_log_quantity_log_uuid_log")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["quantity_uuid"],
|
||||
["quantity.uuid"],
|
||||
name=op.f("fk_log_quantity_quantity_uuid_quantity"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log_quantity")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_quantity_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"log_uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"quantity_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_log_quantity_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_quantity_version_end_transaction_id"),
|
||||
"log_quantity_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_quantity_version_operation_type"),
|
||||
"log_quantity_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_quantity_version_pk_transaction_id",
|
||||
"log_quantity_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_quantity_version_pk_validity",
|
||||
"log_quantity_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_quantity_version_transaction_id"),
|
||||
"log_quantity_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_quantity
|
||||
op.drop_index(
|
||||
op.f("ix_log_quantity_version_transaction_id"),
|
||||
table_name="log_quantity_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_quantity_version_pk_validity", table_name="log_quantity_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_log_quantity_version_pk_transaction_id", table_name="log_quantity_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_quantity_version_operation_type"),
|
||||
table_name="log_quantity_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_log_quantity_version_end_transaction_id"),
|
||||
table_name="log_quantity_version",
|
||||
)
|
||||
op.drop_table("log_quantity_version")
|
||||
op.drop_table("log_quantity")
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
"""use shared base for Group Assets
|
||||
|
||||
Revision ID: aecfd9175624
|
||||
Revises: 34ec51d80f52
|
||||
Create Date: 2026-02-15 13:57:01.055304
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "aecfd9175624"
|
||||
down_revision: Union[str, None] = "34ec51d80f52"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_group
|
||||
op.create_table(
|
||||
"asset_group",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["asset.uuid"], name=op.f("fk_asset_group_uuid_asset")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_group")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_group_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_group_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_group_version_end_transaction_id"),
|
||||
"asset_group_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_group_version_operation_type"),
|
||||
"asset_group_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_group_version_pk_transaction_id",
|
||||
"asset_group_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_group_version_pk_validity",
|
||||
"asset_group_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_group_version_transaction_id"),
|
||||
"asset_group_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# group
|
||||
op.drop_index(
|
||||
op.f("ix_group_version_end_transaction_id"), table_name="group_version"
|
||||
)
|
||||
op.drop_index(op.f("ix_group_version_operation_type"), table_name="group_version")
|
||||
op.drop_index(
|
||||
op.f("ix_group_version_pk_transaction_id"), table_name="group_version"
|
||||
)
|
||||
op.drop_index(op.f("ix_group_version_pk_validity"), table_name="group_version")
|
||||
op.drop_index(op.f("ix_group_version_transaction_id"), table_name="group_version")
|
||||
op.drop_table("group_version")
|
||||
op.drop_table("group")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# group
|
||||
op.create_table(
|
||||
"group",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
||||
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_group")),
|
||||
sa.UniqueConstraint(
|
||||
"drupal_id",
|
||||
name=op.f("uq_group_drupal_id"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"farmos_uuid",
|
||||
name=op.f("uq_group_farmos_uuid"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"name",
|
||||
name=op.f("uq_group_name"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
)
|
||||
op.create_table(
|
||||
"group_version",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
|
||||
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_group_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_group_version_transaction_id"),
|
||||
"group_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_group_version_pk_validity"),
|
||||
"group_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_group_version_pk_transaction_id"),
|
||||
"group_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_group_version_operation_type"),
|
||||
"group_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_group_version_end_transaction_id"),
|
||||
"group_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_group
|
||||
op.drop_index(
|
||||
op.f("ix_asset_group_version_transaction_id"), table_name="asset_group_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_group_version_pk_validity", table_name="asset_group_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_group_version_pk_transaction_id", table_name="asset_group_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_group_version_operation_type"), table_name="asset_group_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_group_version_end_transaction_id"),
|
||||
table_name="asset_group_version",
|
||||
)
|
||||
op.drop_table("asset_group_version")
|
||||
op.drop_table("asset_group")
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"""remove AnimalType.changed
|
||||
|
||||
Revision ID: b8cd4a8f981f
|
||||
Revises: aecfd9175624
|
||||
Create Date: 2026-02-17 18:11:06.110003
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b8cd4a8f981f"
|
||||
down_revision: Union[str, None] = "aecfd9175624"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# animal_type
|
||||
op.drop_column("animal_type", "changed")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# animal_type
|
||||
op.add_column(
|
||||
"animal_type",
|
||||
sa.Column(
|
||||
"changed", postgresql.TIMESTAMP(), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"""add MedicalLog.vet
|
||||
|
||||
Revision ID: d459db991404
|
||||
Revises: 9e875e5cbdc1
|
||||
Create Date: 2026-02-28 22:17:57.001134
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d459db991404"
|
||||
down_revision: Union[str, None] = "9e875e5cbdc1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log_medical
|
||||
op.add_column("log_medical", sa.Column("vet", sa.String(length=100), nullable=True))
|
||||
op.add_column(
|
||||
"log_medical_version",
|
||||
sa.Column("vet", sa.String(length=100), autoincrement=False, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_medical
|
||||
op.drop_column("log_medical_version", "vet")
|
||||
op.drop_column("log_medical", "vet")
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
"""add generic, animal assets
|
||||
|
||||
Revision ID: d6e8d16d6854
|
||||
Revises: 554e6168c339
|
||||
Create Date: 2026-02-15 09:11:04.886362
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d6e8d16d6854"
|
||||
down_revision: Union[str, None] = "554e6168c339"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# animal
|
||||
op.drop_table("animal")
|
||||
op.drop_index(
|
||||
op.f("ix_animal_version_end_transaction_id"), table_name="animal_version"
|
||||
)
|
||||
op.drop_index(op.f("ix_animal_version_operation_type"), table_name="animal_version")
|
||||
op.drop_index(
|
||||
op.f("ix_animal_version_pk_transaction_id"), table_name="animal_version"
|
||||
)
|
||||
op.drop_index(op.f("ix_animal_version_pk_validity"), table_name="animal_version")
|
||||
op.drop_index(op.f("ix_animal_version_transaction_id"), table_name="animal_version")
|
||||
op.drop_table("animal_version")
|
||||
|
||||
# asset
|
||||
op.create_table(
|
||||
"asset",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||
sa.Column("drupal_id", sa.Integer(), nullable=True),
|
||||
sa.Column("asset_type", sa.String(length=100), nullable=False),
|
||||
sa.Column("asset_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("is_location", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_fixed", sa.Boolean(), nullable=False),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("thumbnail_url", sa.String(length=255), nullable=True),
|
||||
sa.Column("image_url", sa.String(length=255), nullable=True),
|
||||
sa.Column("archived", sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["asset_type"],
|
||||
["asset_type.drupal_id"],
|
||||
name=op.f("fk_asset_asset_type_asset_type"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset")),
|
||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_asset_drupal_id")),
|
||||
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_asset_farmos_uuid")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"farmos_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("drupal_id", sa.Integer(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"asset_type", sa.String(length=100), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"asset_name", sa.String(length=100), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("is_location", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column("is_fixed", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column("notes", sa.Text(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column(
|
||||
"image_url", sa.String(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("archived", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_version_end_transaction_id"),
|
||||
"asset_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_version_operation_type"),
|
||||
"asset_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_version_pk_transaction_id",
|
||||
"asset_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_version_pk_validity",
|
||||
"asset_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_version_transaction_id"),
|
||||
"asset_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_animal
|
||||
op.create_table(
|
||||
"asset_animal",
|
||||
sa.Column("animal_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("birthdate", sa.DateTime(), nullable=True),
|
||||
sa.Column("sex", sa.String(length=1), nullable=True),
|
||||
sa.Column("is_sterile", sa.Boolean(), nullable=True),
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["animal_type_uuid"],
|
||||
["animal_type.uuid"],
|
||||
name=op.f("fk_asset_animal_animal_type_uuid_animal_type"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["asset.uuid"], name=op.f("fk_asset_animal_uuid_asset")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_animal")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_animal_version",
|
||||
sa.Column(
|
||||
"animal_type_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("birthdate", sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column("sex", sa.String(length=1), autoincrement=False, nullable=True),
|
||||
sa.Column("is_sterile", sa.Boolean(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_animal_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_animal_version_end_transaction_id"),
|
||||
"asset_animal_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_animal_version_operation_type"),
|
||||
"asset_animal_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_animal_version_pk_transaction_id",
|
||||
"asset_animal_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_animal_version_pk_validity",
|
||||
"asset_animal_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_animal_version_transaction_id"),
|
||||
"asset_animal_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# asset_animal
|
||||
op.drop_index(
|
||||
op.f("ix_asset_animal_version_transaction_id"),
|
||||
table_name="asset_animal_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_animal_version_pk_validity", table_name="asset_animal_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_animal_version_pk_transaction_id", table_name="asset_animal_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_animal_version_operation_type"),
|
||||
table_name="asset_animal_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_animal_version_end_transaction_id"),
|
||||
table_name="asset_animal_version",
|
||||
)
|
||||
op.drop_table("asset_animal_version")
|
||||
op.drop_table("asset_animal")
|
||||
|
||||
# asset
|
||||
op.drop_index(op.f("ix_asset_version_transaction_id"), table_name="asset_version")
|
||||
op.drop_index("ix_asset_version_pk_validity", table_name="asset_version")
|
||||
op.drop_index("ix_asset_version_pk_transaction_id", table_name="asset_version")
|
||||
op.drop_index(op.f("ix_asset_version_operation_type"), table_name="asset_version")
|
||||
op.drop_index(
|
||||
op.f("ix_asset_version_end_transaction_id"), table_name="asset_version"
|
||||
)
|
||||
op.drop_table("asset_version")
|
||||
op.drop_table("asset")
|
||||
|
||||
# animal
|
||||
op.create_table(
|
||||
"animal",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
||||
sa.Column("animal_type_uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("birthdate", sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column("sex", sa.VARCHAR(length=1), autoincrement=False, nullable=True),
|
||||
sa.Column("is_sterile", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["animal_type_uuid"],
|
||||
["animal_type.uuid"],
|
||||
name=op.f("fk_animal_animal_type_uuid_animal_type"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_animal")),
|
||||
sa.UniqueConstraint(
|
||||
"drupal_id",
|
||||
name=op.f("uq_animal_drupal_id"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"farmos_uuid",
|
||||
name=op.f("uq_animal_farmos_uuid"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
)
|
||||
op.create_table(
|
||||
"animal_version",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
|
||||
sa.Column("animal_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"birthdate", postgresql.TIMESTAMP(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("sex", sa.VARCHAR(length=1), autoincrement=False, nullable=True),
|
||||
sa.Column("is_sterile", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column(
|
||||
"image_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"thumbnail_url", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_animal_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_animal_version_transaction_id"),
|
||||
"animal_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_animal_version_pk_validity"),
|
||||
"animal_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_animal_version_pk_transaction_id"),
|
||||
"animal_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_animal_version_operation_type"),
|
||||
"animal_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_animal_version_end_transaction_id"),
|
||||
"animal_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
"""use shared base for Land Assets
|
||||
|
||||
Revision ID: d882682c82f9
|
||||
Revises: d6e8d16d6854
|
||||
Create Date: 2026-02-15 12:00:27.036011
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d882682c82f9"
|
||||
down_revision: Union[str, None] = "d6e8d16d6854"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# asset_parent
|
||||
op.create_table(
|
||||
"asset_parent",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("asset_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("parent_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["asset_uuid"],
|
||||
["asset.uuid"],
|
||||
name=op.f("fk_asset_parent_asset_uuid_asset"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_uuid"],
|
||||
["asset.uuid"],
|
||||
name=op.f("fk_asset_parent_parent_uuid_asset"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_parent")),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_parent_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"asset_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"parent_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_parent_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_parent_version_end_transaction_id"),
|
||||
"asset_parent_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_parent_version_operation_type"),
|
||||
"asset_parent_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_parent_version_pk_transaction_id",
|
||||
"asset_parent_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_parent_version_pk_validity",
|
||||
"asset_parent_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_parent_version_transaction_id"),
|
||||
"asset_parent_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_land
|
||||
op.create_table(
|
||||
"asset_land",
|
||||
sa.Column("land_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["land_type_uuid"],
|
||||
["land_type.uuid"],
|
||||
name=op.f("fk_asset_land_land_type_uuid_land_type"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["uuid"], ["asset.uuid"], name=op.f("fk_asset_land_uuid_asset")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_asset_land")),
|
||||
sa.UniqueConstraint(
|
||||
"land_type_uuid", name=op.f("uq_asset_land_land_type_uuid")
|
||||
),
|
||||
)
|
||||
op.create_table(
|
||||
"asset_land_version",
|
||||
sa.Column(
|
||||
"land_type_uuid",
|
||||
wuttjamaican.db.util.UUID(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_asset_land_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_land_version_end_transaction_id"),
|
||||
"asset_land_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_land_version_operation_type"),
|
||||
"asset_land_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_land_version_pk_transaction_id",
|
||||
"asset_land_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_asset_land_version_pk_validity",
|
||||
"asset_land_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_asset_land_version_transaction_id"),
|
||||
"asset_land_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# land_asset_parent
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_end_transaction_id"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_operation_type"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_pk_transaction_id"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_pk_validity"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_parent_version_transaction_id"),
|
||||
table_name="land_asset_parent_version",
|
||||
)
|
||||
op.drop_table("land_asset_parent_version")
|
||||
op.drop_table("land_asset_parent")
|
||||
|
||||
# land_asset
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_version_end_transaction_id"),
|
||||
table_name="land_asset_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_version_operation_type"), table_name="land_asset_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_version_pk_transaction_id"), table_name="land_asset_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_version_pk_validity"), table_name="land_asset_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_land_asset_version_transaction_id"), table_name="land_asset_version"
|
||||
)
|
||||
op.drop_table("land_asset_version")
|
||||
op.drop_table("land_asset")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# land_asset
|
||||
op.create_table(
|
||||
"land_asset",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
||||
sa.Column("land_type_uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["land_type_uuid"],
|
||||
["land_type.uuid"],
|
||||
name=op.f("fk_land_asset_land_type_uuid_land_type"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset")),
|
||||
sa.UniqueConstraint(
|
||||
"drupal_id",
|
||||
name=op.f("uq_land_asset_drupal_id"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"farmos_uuid",
|
||||
name=op.f("uq_land_asset_farmos_uuid"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"land_type_uuid",
|
||||
name=op.f("uq_land_asset_land_type_uuid"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"name",
|
||||
name=op.f("uq_land_asset_name"),
|
||||
postgresql_include=[],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
),
|
||||
)
|
||||
op.create_table(
|
||||
"land_asset_version",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(length=100), autoincrement=False, nullable=True),
|
||||
sa.Column("land_type_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("is_location", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("is_fixed", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column("archived", sa.BOOLEAN(), autoincrement=False, nullable=True),
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_land_asset_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_version_transaction_id"),
|
||||
"land_asset_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_version_pk_validity"),
|
||||
"land_asset_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_version_pk_transaction_id"),
|
||||
"land_asset_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_version_operation_type"),
|
||||
"land_asset_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_version_end_transaction_id"),
|
||||
"land_asset_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# land_asset_parent
|
||||
op.create_table(
|
||||
"land_asset_parent",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("land_asset_uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("parent_asset_uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["land_asset_uuid"],
|
||||
["land_asset.uuid"],
|
||||
name=op.f("fk_land_asset_parent_land_asset_uuid_land_asset"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_asset_uuid"],
|
||||
["land_asset.uuid"],
|
||||
name=op.f("fk_land_asset_parent_parent_asset_uuid_land_asset"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_land_asset_parent")),
|
||||
)
|
||||
op.create_table(
|
||||
"land_asset_parent_version",
|
||||
sa.Column("uuid", sa.UUID(), autoincrement=False, nullable=False),
|
||||
sa.Column("land_asset_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("parent_asset_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False),
|
||||
sa.Column(
|
||||
"end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"uuid", "transaction_id", name=op.f("pk_land_asset_parent_version")
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_transaction_id"),
|
||||
"land_asset_parent_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_pk_validity"),
|
||||
"land_asset_parent_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_pk_transaction_id"),
|
||||
"land_asset_parent_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_operation_type"),
|
||||
"land_asset_parent_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_land_asset_parent_version_end_transaction_id"),
|
||||
"land_asset_parent_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# asset_land
|
||||
op.drop_table("asset_land")
|
||||
op.drop_index(
|
||||
op.f("ix_asset_land_version_transaction_id"), table_name="asset_land_version"
|
||||
)
|
||||
op.drop_index("ix_asset_land_version_pk_validity", table_name="asset_land_version")
|
||||
op.drop_index(
|
||||
"ix_asset_land_version_pk_transaction_id", table_name="asset_land_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_land_version_operation_type"), table_name="asset_land_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_land_version_end_transaction_id"),
|
||||
table_name="asset_land_version",
|
||||
)
|
||||
op.drop_table("asset_land_version")
|
||||
|
||||
# asset_parent
|
||||
op.drop_index(
|
||||
op.f("ix_asset_parent_version_transaction_id"),
|
||||
table_name="asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_parent_version_pk_validity", table_name="asset_parent_version"
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_asset_parent_version_pk_transaction_id", table_name="asset_parent_version"
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_parent_version_operation_type"),
|
||||
table_name="asset_parent_version",
|
||||
)
|
||||
op.drop_index(
|
||||
op.f("ix_asset_parent_version_end_transaction_id"),
|
||||
table_name="asset_parent_version",
|
||||
)
|
||||
op.drop_table("asset_parent_version")
|
||||
op.drop_table("asset_parent")
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
"""add generic log base
|
||||
|
||||
Revision ID: dd6351e69233
|
||||
Revises: b8cd4a8f981f
|
||||
Create Date: 2026-02-18 12:09:05.200134
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "dd6351e69233"
|
||||
down_revision: Union[str, None] = "b8cd4a8f981f"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log
|
||||
op.create_table(
|
||||
"log",
|
||||
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
|
||||
sa.Column("log_type", sa.String(length=100), nullable=False),
|
||||
sa.Column("message", sa.String(length=255), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(), nullable=False),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True),
|
||||
sa.Column("drupal_id", sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["log_type"], ["log_type.drupal_id"], name=op.f("fk_log_log_type_log_type")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_log")),
|
||||
sa.UniqueConstraint("drupal_id", name=op.f("uq_log_drupal_id")),
|
||||
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_log_farmos_uuid")),
|
||||
)
|
||||
op.create_table(
|
||||
"log_version",
|
||||
sa.Column(
|
||||
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"log_type", sa.String(length=100), autoincrement=False, nullable=True
|
||||
),
|
||||
sa.Column("message", sa.String(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column("timestamp", sa.DateTime(), autoincrement=False, nullable=True),
|
||||
sa.Column("status", sa.String(length=20), autoincrement=False, nullable=True),
|
||||
sa.Column("notes", sa.Text(), 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_log_version")),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_version_end_transaction_id"),
|
||||
"log_version",
|
||||
["end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_version_operation_type"),
|
||||
"log_version",
|
||||
["operation_type"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_version_pk_transaction_id",
|
||||
"log_version",
|
||||
["uuid", sa.literal_column("transaction_id DESC")],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_log_version_pk_validity",
|
||||
"log_version",
|
||||
["uuid", "transaction_id", "end_transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_log_version_transaction_id"),
|
||||
"log_version",
|
||||
["transaction_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
# log_activity
|
||||
op.drop_column("log_activity_version", "status")
|
||||
op.drop_column("log_activity_version", "farmos_uuid")
|
||||
op.drop_column("log_activity_version", "timestamp")
|
||||
op.drop_column("log_activity_version", "message")
|
||||
op.drop_column("log_activity_version", "drupal_id")
|
||||
op.drop_column("log_activity_version", "notes")
|
||||
op.drop_constraint(
|
||||
op.f("uq_log_activity_drupal_id"), "log_activity", type_="unique"
|
||||
)
|
||||
op.drop_constraint(
|
||||
op.f("uq_log_activity_farmos_uuid"), "log_activity", type_="unique"
|
||||
)
|
||||
op.create_foreign_key(
|
||||
op.f("fk_log_activity_uuid_log"), "log_activity", "log", ["uuid"], ["uuid"]
|
||||
)
|
||||
op.drop_column("log_activity", "status")
|
||||
op.drop_column("log_activity", "farmos_uuid")
|
||||
op.drop_column("log_activity", "timestamp")
|
||||
op.drop_column("log_activity", "message")
|
||||
op.drop_column("log_activity", "drupal_id")
|
||||
op.drop_column("log_activity", "notes")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log_activity
|
||||
op.add_column(
|
||||
"log_activity",
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity",
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity",
|
||||
sa.Column(
|
||||
"message", sa.VARCHAR(length=255), autoincrement=False, nullable=False
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity",
|
||||
sa.Column(
|
||||
"timestamp", postgresql.TIMESTAMP(), autoincrement=False, nullable=False
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity",
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity",
|
||||
sa.Column("status", sa.VARCHAR(length=20), autoincrement=False, nullable=False),
|
||||
)
|
||||
op.drop_constraint(
|
||||
op.f("fk_log_activity_uuid_log"), "log_activity", type_="foreignkey"
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
op.f("uq_log_activity_farmos_uuid"),
|
||||
"log_activity",
|
||||
["farmos_uuid"],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
op.f("uq_log_activity_drupal_id"),
|
||||
"log_activity",
|
||||
["drupal_id"],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity_version",
|
||||
sa.Column("notes", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity_version",
|
||||
sa.Column("drupal_id", sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity_version",
|
||||
sa.Column(
|
||||
"message", sa.VARCHAR(length=255), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity_version",
|
||||
sa.Column(
|
||||
"timestamp", postgresql.TIMESTAMP(), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity_version",
|
||||
sa.Column("farmos_uuid", sa.UUID(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"log_activity_version",
|
||||
sa.Column("status", sa.VARCHAR(length=20), autoincrement=False, nullable=True),
|
||||
)
|
||||
|
||||
# log
|
||||
op.drop_index(op.f("ix_log_version_transaction_id"), table_name="log_version")
|
||||
op.drop_index("ix_log_version_pk_validity", table_name="log_version")
|
||||
op.drop_index("ix_log_version_pk_transaction_id", table_name="log_version")
|
||||
op.drop_index(op.f("ix_log_version_operation_type"), table_name="log_version")
|
||||
op.drop_index(op.f("ix_log_version_end_transaction_id"), table_name="log_version")
|
||||
op.drop_table("log_version")
|
||||
op.drop_table("log")
|
||||
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")
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
"""add Log.is_group_assignment
|
||||
|
||||
Revision ID: f3c7e273bfa3
|
||||
Revises: 47d0ebd84554
|
||||
Create Date: 2026-02-28 20:04:40.700474
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f3c7e273bfa3"
|
||||
down_revision: Union[str, None] = "47d0ebd84554"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# log
|
||||
op.add_column("log", sa.Column("is_group_assignment", sa.Boolean(), nullable=True))
|
||||
op.add_column(
|
||||
"log_version",
|
||||
sa.Column(
|
||||
"is_group_assignment", sa.Boolean(), autoincrement=False, nullable=True
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# log
|
||||
op.drop_column("log_version", "is_group_assignment")
|
||||
op.drop_column("log", "is_group_assignment")
|
||||
|
|
@ -30,9 +30,16 @@ from wuttjamaican.db.model import *
|
|||
from .users import WuttaFarmUser
|
||||
|
||||
# wuttafarm proper models
|
||||
from .assets import AssetType
|
||||
from .land import LandType, LandAsset
|
||||
from .structures import StructureType, Structure
|
||||
from .animals import AnimalType, Animal
|
||||
from .groups import Group
|
||||
from .logs import LogType, ActivityLog
|
||||
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
|
||||
from .asset_animal import AnimalType, AnimalAsset
|
||||
from .asset_group import GroupAsset
|
||||
from .asset_plant import PlantType, PlantAsset, PlantAssetPlantType
|
||||
from .log import LogType, Log, LogAsset, LogGroup, LogLocation, LogQuantity, LogOwner
|
||||
from .log_activity import ActivityLog
|
||||
from .log_harvest import HarvestLog
|
||||
from .log_medical import MedicalLog
|
||||
from .log_observation import ObservationLog
|
||||
|
|
|
|||
303
src/wuttafarm/db/model/asset.py
Normal file
303
src/wuttafarm/db/model/asset.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# -*- 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 Asset Types
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
|
||||
class AssetType(model.Base):
|
||||
"""
|
||||
Represents an "asset type" from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_type"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Asset Type",
|
||||
"model_title_plural": "Asset Types",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the asset type.
|
||||
""",
|
||||
)
|
||||
|
||||
description = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Description for the asset type.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the asset type within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.String(length=50),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the asset type.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class Asset(model.Base):
|
||||
"""
|
||||
Represents an asset (of any kind) from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "asset"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Asset",
|
||||
"model_title_plural": "All Assets",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the asset within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the asset.
|
||||
""",
|
||||
)
|
||||
|
||||
asset_type = sa.Column(
|
||||
sa.String(length=100), sa.ForeignKey("asset_type.drupal_id"), nullable=False
|
||||
)
|
||||
|
||||
asset_name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Name of the asset.
|
||||
""",
|
||||
)
|
||||
|
||||
is_location = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
doc="""
|
||||
Whether the asset should be considered a location.
|
||||
""",
|
||||
)
|
||||
|
||||
is_fixed = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
doc="""
|
||||
Whether the asset's location is fixed.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Notes for the asset.
|
||||
""",
|
||||
)
|
||||
|
||||
thumbnail_url = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Optional thumbnail URL for the asset.
|
||||
""",
|
||||
)
|
||||
|
||||
image_url = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Optional image URL for the asset.
|
||||
""",
|
||||
)
|
||||
|
||||
archived = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
doc="""
|
||||
Whether the asset is archived.
|
||||
""",
|
||||
)
|
||||
|
||||
_parents = orm.relationship(
|
||||
"AssetParent",
|
||||
foreign_keys="AssetParent.asset_uuid",
|
||||
back_populates="asset",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
)
|
||||
|
||||
parents = association_proxy(
|
||||
"_parents",
|
||||
"parent",
|
||||
creator=lambda parent: AssetParent(parent=parent),
|
||||
)
|
||||
|
||||
_owners = orm.relationship(
|
||||
"AssetOwner",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="asset",
|
||||
)
|
||||
|
||||
owners = association_proxy(
|
||||
"_owners",
|
||||
"user",
|
||||
creator=lambda user: AssetOwner(user=user),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.asset_name or ""
|
||||
|
||||
|
||||
class AssetMixin:
|
||||
|
||||
uuid = model.uuid_fk_column("asset.uuid", nullable=False, primary_key=True)
|
||||
|
||||
@declared_attr
|
||||
def asset(cls):
|
||||
return orm.relationship(
|
||||
Asset,
|
||||
single_parent=True,
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.asset_name or ""
|
||||
|
||||
|
||||
def add_asset_proxies(subclass):
|
||||
Asset.make_proxy(subclass, "asset", "farmos_uuid")
|
||||
Asset.make_proxy(subclass, "asset", "drupal_id")
|
||||
Asset.make_proxy(subclass, "asset", "asset_type")
|
||||
Asset.make_proxy(subclass, "asset", "asset_name")
|
||||
Asset.make_proxy(subclass, "asset", "is_location")
|
||||
Asset.make_proxy(subclass, "asset", "is_fixed")
|
||||
Asset.make_proxy(subclass, "asset", "notes")
|
||||
Asset.make_proxy(subclass, "asset", "thumbnail_url")
|
||||
Asset.make_proxy(subclass, "asset", "image_url")
|
||||
Asset.make_proxy(subclass, "asset", "archived")
|
||||
Asset.make_proxy(subclass, "asset", "parents")
|
||||
Asset.make_proxy(subclass, "asset", "owners")
|
||||
|
||||
|
||||
class EggMixin:
|
||||
|
||||
produces_eggs = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Whether the group asset produces eggs (i.e. it should be
|
||||
available in the egg harvest form).
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
class AssetParent(model.Base):
|
||||
"""
|
||||
Represents an "asset's parent relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_parent"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
|
||||
|
||||
asset = orm.relationship(
|
||||
Asset,
|
||||
foreign_keys=asset_uuid,
|
||||
)
|
||||
|
||||
parent_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
|
||||
|
||||
parent = orm.relationship(
|
||||
Asset,
|
||||
foreign_keys=parent_uuid,
|
||||
)
|
||||
|
||||
|
||||
class AssetOwner(model.Base):
|
||||
"""
|
||||
Represents a "asset's owner relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_owner"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
|
||||
asset = orm.relationship(
|
||||
Asset,
|
||||
foreign_keys=asset_uuid,
|
||||
back_populates="_owners",
|
||||
)
|
||||
|
||||
user_uuid = model.uuid_fk_column("user.uuid", nullable=False)
|
||||
user = orm.relationship(
|
||||
model.User,
|
||||
foreign_keys=user_uuid,
|
||||
)
|
||||
|
|
@ -28,6 +28,8 @@ from sqlalchemy import orm
|
|||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies, EggMixin
|
||||
|
||||
|
||||
class AnimalType(model.Base):
|
||||
"""
|
||||
|
|
@ -35,11 +37,7 @@ class AnimalType(model.Base):
|
|||
"""
|
||||
|
||||
__tablename__ = "animal_type"
|
||||
__versioned__ = {
|
||||
"exclude": [
|
||||
"changed",
|
||||
],
|
||||
}
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Animal Type",
|
||||
"model_title_plural": "Animal Types",
|
||||
|
|
@ -50,7 +48,6 @@ class AnimalType(model.Base):
|
|||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the animal type.
|
||||
""",
|
||||
|
|
@ -64,14 +61,6 @@ class AnimalType(model.Base):
|
|||
""",
|
||||
)
|
||||
|
||||
changed = sa.Column(
|
||||
sa.DateTime(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
When the animal type was last changed, according to farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
|
|
@ -90,38 +79,38 @@ class AnimalType(model.Base):
|
|||
""",
|
||||
)
|
||||
|
||||
animal_assets = orm.relationship(
|
||||
"AnimalAsset",
|
||||
doc="""
|
||||
List of animal assets of this type.
|
||||
""",
|
||||
back_populates="animal_type",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class Animal(model.Base):
|
||||
class AnimalAsset(AssetMixin, EggMixin, model.Base):
|
||||
"""
|
||||
Represents an animal from farmOS
|
||||
Represents an animal asset from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "animal"
|
||||
__tablename__ = "asset_animal"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Animal",
|
||||
"model_title_plural": "Animals",
|
||||
"model_title": "Animal Asset",
|
||||
"model_title_plural": "Animal Assets",
|
||||
"farmos_asset_type": "animal",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Name for the animal.
|
||||
""",
|
||||
)
|
||||
|
||||
animal_type_uuid = model.uuid_fk_column("animal_type.uuid", nullable=False)
|
||||
animal_type = orm.relationship(
|
||||
"AnimalType",
|
||||
doc="""
|
||||
Reference to the animal type.
|
||||
""",
|
||||
back_populates="animal_assets",
|
||||
)
|
||||
|
||||
birthdate = sa.Column(
|
||||
|
|
@ -148,47 +137,5 @@ class Animal(model.Base):
|
|||
""",
|
||||
)
|
||||
|
||||
active = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the animal is currently active.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Arbitrary notes for the animal.
|
||||
""",
|
||||
)
|
||||
|
||||
image_url = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Optional image URL for the animal.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the animal within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the animal.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
add_asset_proxies(AnimalAsset)
|
||||
45
src/wuttafarm/db/model/asset_group.py
Normal file
45
src/wuttafarm/db/model/asset_group.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Groups
|
||||
"""
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies, EggMixin
|
||||
|
||||
|
||||
class GroupAsset(AssetMixin, EggMixin, model.Base):
|
||||
"""
|
||||
Represents a group asset from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_group"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Group Asset",
|
||||
"model_title_plural": "Group Assets",
|
||||
"farmos_asset_type": "group",
|
||||
}
|
||||
|
||||
|
||||
add_asset_proxies(GroupAsset)
|
||||
|
|
@ -28,6 +28,8 @@ from sqlalchemy import orm
|
|||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies
|
||||
|
||||
|
||||
class LandType(model.Base):
|
||||
"""
|
||||
|
|
@ -76,81 +78,21 @@ class LandType(model.Base):
|
|||
return self.name or ""
|
||||
|
||||
|
||||
class LandAsset(model.Base):
|
||||
class LandAsset(AssetMixin, model.Base):
|
||||
"""
|
||||
Represents a "land asset" from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "land_asset"
|
||||
__tablename__ = "asset_land"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Land Asset",
|
||||
"model_title_plural": "Land Assets",
|
||||
"farmos_asset_type": "land",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the land asset.
|
||||
""",
|
||||
)
|
||||
|
||||
land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False, unique=True)
|
||||
land_type_uuid = model.uuid_fk_column("land_type.uuid", nullable=False)
|
||||
land_type = orm.relationship(LandType, back_populates="land_assets")
|
||||
|
||||
is_location = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the land asset should be considered a location.
|
||||
""",
|
||||
)
|
||||
|
||||
is_fixed = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the land asset's location is fixed.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Notes for the land asset.
|
||||
""",
|
||||
)
|
||||
|
||||
active = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the land asset is currently active.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the land asset within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the land asset.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
add_asset_proxies(LandAsset)
|
||||
|
|
@ -20,25 +20,28 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Log Types
|
||||
Model definition for Plant Assets
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies
|
||||
|
||||
class LogType(model.Base):
|
||||
|
||||
class PlantType(model.Base):
|
||||
"""
|
||||
Represents a "log type" from farmOS
|
||||
Represents a "plant type" (taxonomy term) from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_type"
|
||||
__tablename__ = "plant_type"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Log Type",
|
||||
"model_title_plural": "Log Types",
|
||||
"model_title": "Plant Type",
|
||||
"model_title_plural": "Plant Types",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
|
@ -48,7 +51,7 @@ class LogType(model.Base):
|
|||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the log type.
|
||||
Name of the plant type.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -56,7 +59,7 @@ class LogType(model.Base):
|
|||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Optional description for the log type.
|
||||
Optional description for the plant type.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -65,75 +68,7 @@ class LogType(model.Base):
|
|||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the log type within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.String(length=50),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the log type.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class ActivityLog(model.Base):
|
||||
"""
|
||||
Represents an activity log from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_activity"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Activity Log",
|
||||
"model_title_plural": "Activity Logs",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
message = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Message text for the log.
|
||||
""",
|
||||
)
|
||||
|
||||
timestamp = sa.Column(
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Date and time when the log event occurred / will occur.
|
||||
""",
|
||||
)
|
||||
|
||||
status = sa.Column(
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Current status of the log event.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Arbitrary notes for the log event.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the log within farmOS.
|
||||
UUID for the plant type within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -142,9 +77,72 @@ class ActivityLog(model.Base):
|
|||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the log.
|
||||
Drupal internal ID for the plant type.
|
||||
""",
|
||||
)
|
||||
|
||||
_plant_assets = orm.relationship(
|
||||
"PlantAssetPlantType",
|
||||
cascade_backrefs=False,
|
||||
back_populates="plant_type",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.message or ""
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class PlantAsset(AssetMixin, model.Base):
|
||||
"""
|
||||
Represents a plant asset from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_plant"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Plant Asset",
|
||||
"model_title_plural": "Plant Assets",
|
||||
"farmos_asset_type": "plant",
|
||||
}
|
||||
|
||||
_plant_types = orm.relationship(
|
||||
"PlantAssetPlantType",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="plant_asset",
|
||||
)
|
||||
|
||||
plant_types = association_proxy(
|
||||
"_plant_types",
|
||||
"plant_type",
|
||||
creator=lambda pt: PlantAssetPlantType(plant_type=pt),
|
||||
)
|
||||
|
||||
|
||||
add_asset_proxies(PlantAsset)
|
||||
|
||||
|
||||
class PlantAssetPlantType(model.Base):
|
||||
"""
|
||||
Associates one or more plant types with a plant asset.
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_plant_plant_type"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
plant_asset_uuid = model.uuid_fk_column("asset_plant.uuid", nullable=False)
|
||||
plant_asset = orm.relationship(
|
||||
PlantAsset,
|
||||
foreign_keys=plant_asset_uuid,
|
||||
back_populates="_plant_types",
|
||||
)
|
||||
|
||||
plant_type_uuid = model.uuid_fk_column("plant_type.uuid", nullable=False)
|
||||
plant_type = orm.relationship(
|
||||
PlantType,
|
||||
doc="""
|
||||
Reference to the plant type.
|
||||
""",
|
||||
back_populates="_plant_assets",
|
||||
)
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Asset Types
|
||||
Model definition for Structure Types
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -28,17 +28,19 @@ from sqlalchemy import orm
|
|||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.asset import AssetMixin, add_asset_proxies
|
||||
|
||||
class AssetType(model.Base):
|
||||
|
||||
class StructureType(model.Base):
|
||||
"""
|
||||
Represents an "asset type" from farmOS
|
||||
Represents a "structure type" from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_type"
|
||||
__tablename__ = "structure_type"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Asset Type",
|
||||
"model_title_plural": "Asset Types",
|
||||
"model_title": "Structure Type",
|
||||
"model_title_plural": "Structure Types",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
|
@ -48,15 +50,7 @@ class AssetType(model.Base):
|
|||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the asset type.
|
||||
""",
|
||||
)
|
||||
|
||||
description = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Description for the asset type.
|
||||
Name of the structure type.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -65,7 +59,7 @@ class AssetType(model.Base):
|
|||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the asset type within farmOS.
|
||||
UUID for the structure type within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -74,9 +68,34 @@ class AssetType(model.Base):
|
|||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the asset type.
|
||||
Drupal internal ID for the structure type.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class StructureAsset(AssetMixin, model.Base):
|
||||
"""
|
||||
Represents a structure from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "asset_structure"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Structure Asset",
|
||||
"model_title_plural": "Structure Assets",
|
||||
"farmos_asset_type": "structure",
|
||||
}
|
||||
|
||||
structure_type_uuid = model.uuid_fk_column("structure_type.uuid", nullable=False)
|
||||
structure_type = orm.relationship(
|
||||
"StructureType",
|
||||
doc="""
|
||||
Reference to the type of structure.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
add_asset_proxies(StructureAsset)
|
||||
404
src/wuttafarm/db/model/log.py
Normal file
404
src/wuttafarm/db/model/log.py
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# -*- 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 Logs
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
|
||||
class LogType(model.Base):
|
||||
"""
|
||||
Represents a "log type" from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_type"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Log Type",
|
||||
"model_title_plural": "Log Types",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the log type.
|
||||
""",
|
||||
)
|
||||
|
||||
description = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Optional description for the log type.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the log type within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.String(length=50),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the log type.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class Log(model.Base):
|
||||
"""
|
||||
Represents a base log record from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Log",
|
||||
"model_title_plural": "All Logs",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
log_type = sa.Column(
|
||||
sa.String(length=100),
|
||||
sa.ForeignKey("log_type.drupal_id"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
message = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Message text for the log.
|
||||
""",
|
||||
)
|
||||
|
||||
timestamp = sa.Column(
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Date and time when the log event occurred / will occur.
|
||||
""",
|
||||
)
|
||||
|
||||
is_movement = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Whether the log represents a movement to new location.
|
||||
""",
|
||||
)
|
||||
|
||||
is_group_assignment = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Whether the log represents a group assignment.
|
||||
""",
|
||||
)
|
||||
|
||||
status = sa.Column(
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Current status of the log event.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Arbitrary notes for the log event.
|
||||
""",
|
||||
)
|
||||
|
||||
quick = sa.Column(
|
||||
sa.String(length=20),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Identifier of quick form used to create the log, if
|
||||
applicable.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the log within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the log.
|
||||
""",
|
||||
)
|
||||
|
||||
_assets = orm.relationship(
|
||||
"LogAsset",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="log",
|
||||
)
|
||||
|
||||
assets = association_proxy(
|
||||
"_assets",
|
||||
"asset",
|
||||
creator=lambda asset: LogAsset(asset=asset),
|
||||
)
|
||||
|
||||
_groups = orm.relationship(
|
||||
"LogGroup",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="log",
|
||||
)
|
||||
|
||||
groups = association_proxy(
|
||||
"_groups",
|
||||
"asset",
|
||||
creator=lambda asset: LogGroup(asset=asset),
|
||||
)
|
||||
|
||||
_locations = orm.relationship(
|
||||
"LogLocation",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="log",
|
||||
)
|
||||
|
||||
locations = association_proxy(
|
||||
"_locations",
|
||||
"asset",
|
||||
creator=lambda asset: LogLocation(asset=asset),
|
||||
)
|
||||
|
||||
_quantities = orm.relationship(
|
||||
"LogQuantity",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="log",
|
||||
)
|
||||
|
||||
quantities = association_proxy(
|
||||
"_quantities",
|
||||
"quantity",
|
||||
creator=lambda quantity: LogQuantity(quantity=quantity),
|
||||
)
|
||||
|
||||
_owners = orm.relationship(
|
||||
"LogOwner",
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="log",
|
||||
)
|
||||
|
||||
owners = association_proxy(
|
||||
"_owners",
|
||||
"user",
|
||||
creator=lambda user: LogOwner(user=user),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.message or ""
|
||||
|
||||
|
||||
class LogMixin:
|
||||
|
||||
uuid = model.uuid_fk_column("log.uuid", nullable=False, primary_key=True)
|
||||
|
||||
@declared_attr
|
||||
def log(cls):
|
||||
return orm.relationship(
|
||||
Log,
|
||||
single_parent=True,
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.message or ""
|
||||
|
||||
|
||||
def add_log_proxies(subclass):
|
||||
Log.make_proxy(subclass, "log", "farmos_uuid")
|
||||
Log.make_proxy(subclass, "log", "drupal_id")
|
||||
Log.make_proxy(subclass, "log", "log_type")
|
||||
Log.make_proxy(subclass, "log", "message")
|
||||
Log.make_proxy(subclass, "log", "timestamp")
|
||||
Log.make_proxy(subclass, "log", "is_movement")
|
||||
Log.make_proxy(subclass, "log", "is_group_assignment")
|
||||
Log.make_proxy(subclass, "log", "status")
|
||||
Log.make_proxy(subclass, "log", "notes")
|
||||
Log.make_proxy(subclass, "log", "quick")
|
||||
Log.make_proxy(subclass, "log", "assets")
|
||||
Log.make_proxy(subclass, "log", "groups")
|
||||
Log.make_proxy(subclass, "log", "locations")
|
||||
Log.make_proxy(subclass, "log", "quantities")
|
||||
Log.make_proxy(subclass, "log", "owners")
|
||||
|
||||
|
||||
class LogAsset(model.Base):
|
||||
"""
|
||||
Represents a "log's asset relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "log_asset"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
log_uuid = model.uuid_fk_column("log.uuid", nullable=False)
|
||||
log = orm.relationship(
|
||||
Log,
|
||||
foreign_keys=log_uuid,
|
||||
back_populates="_assets",
|
||||
)
|
||||
|
||||
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
|
||||
asset = orm.relationship(
|
||||
"Asset",
|
||||
foreign_keys=asset_uuid,
|
||||
)
|
||||
|
||||
|
||||
class LogGroup(model.Base):
|
||||
"""
|
||||
Represents a "log's group relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "log_group"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
log_uuid = model.uuid_fk_column("log.uuid", nullable=False)
|
||||
log = orm.relationship(
|
||||
Log,
|
||||
foreign_keys=log_uuid,
|
||||
back_populates="_groups",
|
||||
)
|
||||
|
||||
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
|
||||
asset = orm.relationship(
|
||||
"Asset",
|
||||
foreign_keys=asset_uuid,
|
||||
)
|
||||
|
||||
|
||||
class LogLocation(model.Base):
|
||||
"""
|
||||
Represents a "log's location relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "log_location"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
log_uuid = model.uuid_fk_column("log.uuid", nullable=False)
|
||||
log = orm.relationship(
|
||||
Log,
|
||||
foreign_keys=log_uuid,
|
||||
back_populates="_locations",
|
||||
)
|
||||
|
||||
asset_uuid = model.uuid_fk_column("asset.uuid", nullable=False)
|
||||
asset = orm.relationship(
|
||||
"Asset",
|
||||
foreign_keys=asset_uuid,
|
||||
)
|
||||
|
||||
|
||||
class LogQuantity(model.Base):
|
||||
"""
|
||||
Represents a "log's quantity relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "log_quantity"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
log_uuid = model.uuid_fk_column("log.uuid", nullable=False)
|
||||
log = orm.relationship(
|
||||
Log,
|
||||
foreign_keys=log_uuid,
|
||||
back_populates="_quantities",
|
||||
)
|
||||
|
||||
quantity_uuid = model.uuid_fk_column("quantity.uuid", nullable=False)
|
||||
quantity = orm.relationship(
|
||||
"Quantity",
|
||||
foreign_keys=quantity_uuid,
|
||||
back_populates="_log",
|
||||
)
|
||||
|
||||
|
||||
class LogOwner(model.Base):
|
||||
"""
|
||||
Represents a "log's owner relationship" from farmOS.
|
||||
"""
|
||||
|
||||
__tablename__ = "log_owner"
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
log_uuid = model.uuid_fk_column("log.uuid", nullable=False)
|
||||
log = orm.relationship(
|
||||
Log,
|
||||
foreign_keys=log_uuid,
|
||||
back_populates="_owners",
|
||||
)
|
||||
|
||||
user_uuid = model.uuid_fk_column("user.uuid", nullable=False)
|
||||
user = orm.relationship(
|
||||
model.User,
|
||||
foreign_keys=user_uuid,
|
||||
)
|
||||
45
src/wuttafarm/db/model/log_activity.py
Normal file
45
src/wuttafarm/db/model/log_activity.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Activity Logs
|
||||
"""
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.log import LogMixin, add_log_proxies
|
||||
|
||||
|
||||
class ActivityLog(LogMixin, model.Base):
|
||||
"""
|
||||
Represents an Activity Log from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_activity"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Activity Log",
|
||||
"model_title_plural": "Activity Logs",
|
||||
"farmos_log_type": "activity",
|
||||
}
|
||||
|
||||
|
||||
add_log_proxies(ActivityLog)
|
||||
45
src/wuttafarm/db/model/log_harvest.py
Normal file
45
src/wuttafarm/db/model/log_harvest.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Harvest Logs
|
||||
"""
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.log import LogMixin, add_log_proxies
|
||||
|
||||
|
||||
class HarvestLog(LogMixin, model.Base):
|
||||
"""
|
||||
Represents a Harvest Log from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_harvest"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Harvest Log",
|
||||
"model_title_plural": "Harvest Logs",
|
||||
"farmos_log_type": "harvest",
|
||||
}
|
||||
|
||||
|
||||
add_log_proxies(HarvestLog)
|
||||
55
src/wuttafarm/db/model/log_medical.py
Normal file
55
src/wuttafarm/db/model/log_medical.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# -*- 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 Medical Logs
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.log import LogMixin, add_log_proxies
|
||||
|
||||
|
||||
class MedicalLog(LogMixin, model.Base):
|
||||
"""
|
||||
Represents a Medical Log from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_medical"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Medical Log",
|
||||
"model_title_plural": "Medical Logs",
|
||||
"farmos_log_type": "medical",
|
||||
}
|
||||
|
||||
vet = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Name of the veterinarian, if applicable.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
add_log_proxies(MedicalLog)
|
||||
45
src/wuttafarm/db/model/log_observation.py
Normal file
45
src/wuttafarm/db/model/log_observation.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Observation Logs
|
||||
"""
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
from wuttafarm.db.model.log import LogMixin, add_log_proxies
|
||||
|
||||
|
||||
class ObservationLog(LogMixin, model.Base):
|
||||
"""
|
||||
Represents a Observation Log from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "log_observation"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Observation Log",
|
||||
"model_title_plural": "Observation Logs",
|
||||
"farmos_log_type": "observation",
|
||||
}
|
||||
|
||||
|
||||
add_log_proxies(ObservationLog)
|
||||
242
src/wuttafarm/db/model/quantities.py
Normal file
242
src/wuttafarm/db/model/quantities.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# -*- 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 sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
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.
|
||||
""",
|
||||
)
|
||||
|
||||
_log = orm.relationship(
|
||||
"LogQuantity",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
cascade_backrefs=False,
|
||||
back_populates="quantity",
|
||||
)
|
||||
|
||||
def make_log_quantity(log):
|
||||
from wuttafarm.db.model import LogQuantity
|
||||
|
||||
return LogQuantity(log=log)
|
||||
|
||||
log = association_proxy(
|
||||
"_log",
|
||||
"log",
|
||||
creator=make_log_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")
|
||||
Quantity.make_proxy(subclass, "quantity", "log")
|
||||
|
||||
|
||||
class StandardQuantity(QuantityMixin, model.Base):
|
||||
"""
|
||||
Represents a Standard Quantity from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "quantity_standard"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Standard Quantity",
|
||||
"model_title_plural": "Standard Quantities",
|
||||
"farmos_quantity_type": "standard",
|
||||
}
|
||||
|
||||
|
||||
add_quantity_proxies(StandardQuantity)
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Structure Types
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
|
||||
class StructureType(model.Base):
|
||||
"""
|
||||
Represents a "structure type" from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "structure_type"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Structure Type",
|
||||
"model_title_plural": "Structure Types",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name of the structure type.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the structure type within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.String(length=50),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the structure type.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
||||
|
||||
class Structure(model.Base):
|
||||
"""
|
||||
Represents a structure from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "structure"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Structure",
|
||||
"model_title_plural": "Structures",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
name = sa.Column(
|
||||
sa.String(length=100),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name for the structure.
|
||||
""",
|
||||
)
|
||||
|
||||
active = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the structure is currently active.
|
||||
""",
|
||||
)
|
||||
|
||||
structure_type_uuid = model.uuid_fk_column("structure_type.uuid", nullable=False)
|
||||
structure_type = orm.relationship(
|
||||
"StructureType",
|
||||
doc="""
|
||||
Reference to the type of structure.
|
||||
""",
|
||||
)
|
||||
|
||||
is_location = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the structure is considered a location.
|
||||
""",
|
||||
)
|
||||
|
||||
is_fixed = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the structure location is fixed.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Arbitrary notes for the structure.
|
||||
""",
|
||||
)
|
||||
|
||||
image_url = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Optional image URL for the structure.
|
||||
""",
|
||||
)
|
||||
|
||||
farmos_uuid = sa.Column(
|
||||
model.UUID(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the structure within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
drupal_id = sa.Column(
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the structure.
|
||||
""",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ""
|
||||
|
|
@ -20,25 +20,24 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
Model definition for Groups
|
||||
Model definition for Units
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
from wuttjamaican.db import model
|
||||
|
||||
|
||||
class Group(model.Base):
|
||||
class Measure(model.Base):
|
||||
"""
|
||||
Represents a "group" from farmOS
|
||||
Represents a "measure" option (for quantities) from farmOS
|
||||
"""
|
||||
|
||||
__tablename__ = "group"
|
||||
__tablename__ = "measure"
|
||||
__versioned__ = {}
|
||||
__wutta_hint__ = {
|
||||
"model_title": "Group",
|
||||
"model_title_plural": "Groups",
|
||||
"model_title": "Measure",
|
||||
"model_title_plural": "Measures",
|
||||
}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
|
@ -48,39 +47,51 @@ class Group(model.Base):
|
|||
nullable=False,
|
||||
unique=True,
|
||||
doc="""
|
||||
Name for the group.
|
||||
Name of the measure.
|
||||
""",
|
||||
)
|
||||
|
||||
is_location = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
drupal_id = sa.Column(
|
||||
sa.String(length=20),
|
||||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Whether the group is considered to be a location.
|
||||
Drupal internal ID for the measure.
|
||||
""",
|
||||
)
|
||||
|
||||
is_fixed = sa.Column(
|
||||
sa.Boolean(),
|
||||
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="""
|
||||
Whether the group location is fixed.
|
||||
Name of the unit.
|
||||
""",
|
||||
)
|
||||
|
||||
active = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
doc="""
|
||||
Whether the group is active.
|
||||
""",
|
||||
)
|
||||
|
||||
notes = sa.Column(
|
||||
sa.Text(),
|
||||
description = sa.Column(
|
||||
sa.String(length=255),
|
||||
nullable=True,
|
||||
doc="""
|
||||
Arbitrary notes for the group.
|
||||
Optional description for the unit.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -89,7 +100,7 @@ class Group(model.Base):
|
|||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
UUID for the group within farmOS.
|
||||
UUID for the unit within farmOS.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -98,7 +109,7 @@ class Group(model.Base):
|
|||
nullable=True,
|
||||
unique=True,
|
||||
doc="""
|
||||
Drupal internal ID for the group.
|
||||
Drupal internal ID for the unit.
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -26,6 +26,12 @@ Email sending config for WuttaFarm
|
|||
from wuttasync.emails import ImportExportWarning
|
||||
|
||||
|
||||
class export_to_farmos_from_wuttafarm_warning(ImportExportWarning):
|
||||
"""
|
||||
Diff warning for WuttaFarm → farmOS export.
|
||||
"""
|
||||
|
||||
|
||||
class import_to_wuttafarm_from_farmos_warning(ImportExportWarning):
|
||||
"""
|
||||
Diff warning for farmOS → WuttaFarm import.
|
||||
|
|
|
|||
58
src/wuttafarm/enum.py
Normal file
58
src/wuttafarm/enum.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaFarm enum values
|
||||
"""
|
||||
|
||||
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"),
|
||||
("F", "Female"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
LOG_STATUS = OrderedDict(
|
||||
[
|
||||
("pending", "Pending"),
|
||||
("done", "Done"),
|
||||
("abandoned", "Abandoned"),
|
||||
]
|
||||
)
|
||||
|
|
@ -94,3 +94,9 @@ class FarmOSHandler(GenericHandler):
|
|||
return f"{base}/{path}"
|
||||
|
||||
return base
|
||||
|
||||
def get_oauth2_client_id(self):
|
||||
return self.config.get("farmos.oauth2.client_id", default="farm")
|
||||
|
||||
def get_oauth2_scope(self):
|
||||
return self.config.get("farmos.oauth2.scope", default="farm_manager")
|
||||
|
|
|
|||
26
src/wuttafarm/farmos/importing/__init__.py
Normal file
26
src/wuttafarm/farmos/importing/__init__.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Importing data *into* farmOS
|
||||
"""
|
||||
|
||||
from . import model
|
||||
758
src/wuttafarm/farmos/importing/model.py
Normal file
758
src/wuttafarm/farmos/importing/model.py
Normal file
|
|
@ -0,0 +1,758 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Importer models targeting farmOS
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import requests
|
||||
|
||||
from wuttasync.importing import Importer
|
||||
|
||||
|
||||
class ToFarmOS(Importer):
|
||||
"""
|
||||
Base class for data importer targeting the farmOS API.
|
||||
"""
|
||||
|
||||
key = "uuid"
|
||||
caches_target = True
|
||||
|
||||
def format_datetime(self, dt):
|
||||
"""
|
||||
Convert a WuttaFarm datetime object to the format required for
|
||||
pushing to the farmOS API.
|
||||
"""
|
||||
if dt is None:
|
||||
return None
|
||||
dt = self.app.localtime(dt)
|
||||
return dt.timestamp()
|
||||
|
||||
def normalize_datetime(self, dt):
|
||||
"""
|
||||
Convert a farmOS datetime value to naive UTC used by
|
||||
WuttaFarm.
|
||||
|
||||
:param dt: Date/time string value "as-is" from the farmOS API.
|
||||
|
||||
:returns: Equivalent naive UTC ``datetime``
|
||||
"""
|
||||
if dt is None:
|
||||
return None
|
||||
dt = datetime.datetime.fromisoformat(dt)
|
||||
return self.app.make_utc(dt)
|
||||
|
||||
|
||||
class ToFarmOSTaxonomy(ToFarmOS):
|
||||
|
||||
farmos_taxonomy_type = None
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
]
|
||||
|
||||
def get_target_objects(self, **kwargs):
|
||||
result = self.farmos_client.resource.get(
|
||||
"taxonomy_term", self.farmos_taxonomy_type
|
||||
)
|
||||
return result["data"]
|
||||
|
||||
def get_target_object(self, key):
|
||||
|
||||
# fetch from cache, if applicable
|
||||
if self.caches_target:
|
||||
return super().get_target_object(key)
|
||||
|
||||
# okay now must fetch via API
|
||||
if self.get_keys() != ["uuid"]:
|
||||
raise ValueError("must use uuid key for this to work")
|
||||
uuid = key[0]
|
||||
|
||||
try:
|
||||
result = self.farmos_client.resource.get_id(
|
||||
"taxonomy_term", self.farmos_taxonomy_type, str(uuid)
|
||||
)
|
||||
except requests.HTTPError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return None
|
||||
raise
|
||||
return result["data"]
|
||||
|
||||
def normalize_target_object(self, obj):
|
||||
return {
|
||||
"uuid": UUID(obj["id"]),
|
||||
"name": obj["attributes"]["name"],
|
||||
}
|
||||
|
||||
def get_term_payload(self, source_data):
|
||||
return {
|
||||
"attributes": {
|
||||
"name": source_data["name"],
|
||||
}
|
||||
}
|
||||
|
||||
def create_target_object(self, key, source_data):
|
||||
if source_data.get("__ignoreme__"):
|
||||
return None
|
||||
if self.dry_run:
|
||||
return source_data
|
||||
|
||||
payload = self.get_term_payload(source_data)
|
||||
result = self.farmos_client.resource.send(
|
||||
"taxonomy_term", self.farmos_taxonomy_type, payload
|
||||
)
|
||||
normal = self.normalize_target_object(result["data"])
|
||||
normal["_new_object"] = result["data"]
|
||||
return normal
|
||||
|
||||
def update_target_object(self, asset, source_data, target_data=None):
|
||||
if self.dry_run:
|
||||
return asset
|
||||
|
||||
payload = self.get_term_payload(source_data)
|
||||
payload["id"] = str(source_data["uuid"])
|
||||
result = self.farmos_client.resource.send(
|
||||
"taxonomy_term", self.farmos_taxonomy_type, payload
|
||||
)
|
||||
return self.normalize_target_object(result["data"])
|
||||
|
||||
|
||||
class ToFarmOSAsset(ToFarmOS):
|
||||
"""
|
||||
Base class for asset data importer targeting the farmOS API.
|
||||
"""
|
||||
|
||||
farmos_asset_type = None
|
||||
|
||||
def get_target_objects(self, **kwargs):
|
||||
assets = self.farmos_client.asset.get(self.farmos_asset_type)
|
||||
return assets["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:
|
||||
asset = self.farmos_client.asset.get_id(self.farmos_asset_type, str(uuid))
|
||||
except requests.HTTPError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return None
|
||||
raise
|
||||
return asset["data"]
|
||||
|
||||
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_asset_payload(source_data)
|
||||
result = self.farmos_client.asset.send(self.farmos_asset_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_asset_payload(source_data)
|
||||
payload["id"] = str(source_data["uuid"])
|
||||
result = self.farmos_client.asset.send(self.farmos_asset_type, payload)
|
||||
return self.normalize_target_object(result["data"])
|
||||
|
||||
def normalize_target_object(self, asset):
|
||||
|
||||
if notes := asset["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
|
||||
return {
|
||||
"uuid": UUID(asset["id"]),
|
||||
"asset_name": asset["attributes"]["name"],
|
||||
"is_location": asset["attributes"]["is_location"],
|
||||
"is_fixed": asset["attributes"]["is_fixed"],
|
||||
"produces_eggs": asset["attributes"].get("produces_eggs"),
|
||||
"notes": notes,
|
||||
"archived": asset["attributes"]["archived"],
|
||||
}
|
||||
|
||||
def get_asset_payload(self, source_data):
|
||||
|
||||
attrs = {}
|
||||
if "asset_name" in self.fields:
|
||||
attrs["name"] = source_data["asset_name"]
|
||||
if "is_location" in self.fields:
|
||||
attrs["is_location"] = source_data["is_location"]
|
||||
if "is_fixed" in self.fields:
|
||||
attrs["is_fixed"] = source_data["is_fixed"]
|
||||
if "produces_eggs" in self.fields:
|
||||
attrs["produces_eggs"] = source_data["produces_eggs"]
|
||||
if "notes" in self.fields:
|
||||
attrs["notes"] = {"value": source_data["notes"]}
|
||||
if "archived" in self.fields:
|
||||
attrs["archived"] = source_data["archived"]
|
||||
|
||||
payload = {"attributes": attrs}
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class UnitImporter(ToFarmOSTaxonomy):
|
||||
|
||||
model_title = "Unit"
|
||||
farmos_taxonomy_type = "unit"
|
||||
|
||||
|
||||
class AnimalAssetImporter(ToFarmOSAsset):
|
||||
|
||||
model_title = "AnimalAsset"
|
||||
farmos_asset_type = "animal"
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"animal_type_uuid",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"produces_eggs",
|
||||
"birthdate",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_target_object(self, animal):
|
||||
data = super().normalize_target_object(animal)
|
||||
data.update(
|
||||
{
|
||||
"animal_type_uuid": UUID(
|
||||
animal["relationships"]["animal_type"]["data"]["id"]
|
||||
),
|
||||
"sex": animal["attributes"]["sex"],
|
||||
"is_sterile": animal["attributes"]["is_sterile"],
|
||||
"birthdate": self.normalize_datetime(animal["attributes"]["birthdate"]),
|
||||
}
|
||||
)
|
||||
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 "animal_type_uuid" in self.fields:
|
||||
rels["animal_type"] = {
|
||||
"data": {
|
||||
"id": str(source_data["animal_type_uuid"]),
|
||||
"type": "taxonomy_term--animal_type",
|
||||
}
|
||||
}
|
||||
|
||||
payload["attributes"].update(attrs)
|
||||
if rels:
|
||||
payload.setdefault("relationships", {}).update(rels)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class AnimalTypeImporter(ToFarmOSTaxonomy):
|
||||
|
||||
model_title = "AnimalType"
|
||||
farmos_taxonomy_type = "animal_type"
|
||||
|
||||
|
||||
class GroupAssetImporter(ToFarmOSAsset):
|
||||
|
||||
model_title = "GroupAsset"
|
||||
farmos_asset_type = "group"
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"produces_eggs",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
|
||||
class LandAssetImporter(ToFarmOSAsset):
|
||||
|
||||
model_title = "LandAsset"
|
||||
farmos_asset_type = "land"
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"land_type_id",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_target_object(self, land):
|
||||
data = super().normalize_target_object(land)
|
||||
data.update(
|
||||
{
|
||||
"land_type_id": land["attributes"]["land_type"],
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def get_asset_payload(self, source_data):
|
||||
payload = super().get_asset_payload(source_data)
|
||||
|
||||
attrs = {}
|
||||
if "land_type_id" in self.fields:
|
||||
attrs["land_type"] = source_data["land_type_id"]
|
||||
|
||||
if attrs:
|
||||
payload["attributes"].update(attrs)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class PlantTypeImporter(ToFarmOSTaxonomy):
|
||||
|
||||
model_title = "PlantType"
|
||||
farmos_taxonomy_type = "plant_type"
|
||||
|
||||
|
||||
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"
|
||||
farmos_asset_type = "structure"
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"structure_type_id",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_target_object(self, structure):
|
||||
data = super().normalize_target_object(structure)
|
||||
data.update(
|
||||
{
|
||||
"structure_type_id": structure["attributes"]["structure_type"],
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def get_asset_payload(self, source_data):
|
||||
payload = super().get_asset_payload(source_data)
|
||||
|
||||
attrs = {}
|
||||
if "structure_type_id" in self.fields:
|
||||
attrs["structure_type"] = source_data["structure_type_id"]
|
||||
|
||||
if attrs:
|
||||
payload["attributes"].update(attrs)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
##############################
|
||||
# quantity importers
|
||||
##############################
|
||||
|
||||
|
||||
class ToFarmOSQuantity(ToFarmOS):
|
||||
"""
|
||||
Base class for quantity data importer targeting the farmOS API.
|
||||
"""
|
||||
|
||||
farmos_quantity_type = None
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"measure",
|
||||
"value_numerator",
|
||||
"value_denominator",
|
||||
"label",
|
||||
"quantity_type_uuid",
|
||||
"unit_uuid",
|
||||
]
|
||||
|
||||
def get_target_objects(self, **kwargs):
|
||||
return list(
|
||||
self.farmos_client.resource.iterate("quantity", self.farmos_quantity_type)
|
||||
)
|
||||
|
||||
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:
|
||||
qty = self.farmos_client.resource.get_id(
|
||||
"quantity", self.farmos_quantity_type, str(uuid)
|
||||
)
|
||||
except requests.HTTPError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return None
|
||||
raise
|
||||
return qty["data"]
|
||||
|
||||
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_quantity_payload(source_data)
|
||||
result = self.farmos_client.resource.send(
|
||||
"quantity", self.farmos_quantity_type, payload
|
||||
)
|
||||
normal = self.normalize_target_object(result["data"])
|
||||
normal["_new_object"] = result["data"]
|
||||
return normal
|
||||
|
||||
def update_target_object(self, quantity, source_data, target_data=None):
|
||||
if self.dry_run:
|
||||
return quantity
|
||||
|
||||
payload = self.get_quantity_payload(source_data)
|
||||
payload["id"] = str(source_data["uuid"])
|
||||
result = self.farmos_client.resource.send(
|
||||
"quantity", self.farmos_quantity_type, payload
|
||||
)
|
||||
return self.normalize_target_object(result["data"])
|
||||
|
||||
def normalize_target_object(self, qty):
|
||||
|
||||
result = {
|
||||
"uuid": UUID(qty["id"]),
|
||||
"measure": qty["attributes"]["measure"],
|
||||
"value_numerator": qty["attributes"]["value"]["numerator"],
|
||||
"value_denominator": qty["attributes"]["value"]["denominator"],
|
||||
"label": qty["attributes"]["label"],
|
||||
"quantity_type_uuid": UUID(
|
||||
qty["relationships"]["quantity_type"]["data"]["id"]
|
||||
),
|
||||
"unit_uuid": None,
|
||||
}
|
||||
|
||||
if unit := qty["relationships"]["units"]["data"]:
|
||||
result["unit_uuid"] = UUID(unit["id"])
|
||||
|
||||
return result
|
||||
|
||||
def get_quantity_payload(self, source_data):
|
||||
|
||||
attrs = {}
|
||||
if "measure" in self.fields:
|
||||
attrs["measure"] = source_data["measure"]
|
||||
if "value_numerator" in self.fields and "value_denominator" in self.fields:
|
||||
attrs["value"] = {
|
||||
"numerator": source_data["value_numerator"],
|
||||
"denominator": source_data["value_denominator"],
|
||||
}
|
||||
if "label" in self.fields:
|
||||
attrs["label"] = source_data["label"]
|
||||
|
||||
rels = {}
|
||||
if "quantity_type_uuid" in self.fields:
|
||||
rels["quantity_type"] = {
|
||||
"data": {
|
||||
"id": str(source_data["quantity_type_uuid"]),
|
||||
"type": "quantity_type--quantity_type",
|
||||
}
|
||||
}
|
||||
if "unit_uuid" in self.fields:
|
||||
rels["units"] = {
|
||||
"data": {
|
||||
"id": str(source_data["unit_uuid"]),
|
||||
"type": "taxonomy_term--unit",
|
||||
}
|
||||
}
|
||||
|
||||
payload = {"attributes": attrs, "relationships": rels}
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class StandardQuantityImporter(ToFarmOSQuantity):
|
||||
|
||||
model_title = "StandardQuantity"
|
||||
farmos_quantity_type = "standard"
|
||||
|
||||
|
||||
##############################
|
||||
# log importers
|
||||
##############################
|
||||
|
||||
|
||||
class ToFarmOSLog(ToFarmOS):
|
||||
"""
|
||||
Base class for log data importer targeting the farmOS API.
|
||||
"""
|
||||
|
||||
farmos_log_type = None
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
"timestamp",
|
||||
"is_movement",
|
||||
"is_group_assignment",
|
||||
"status",
|
||||
"notes",
|
||||
"quick",
|
||||
"assets",
|
||||
"quantities",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.normal = self.app.get_normalizer(self.farmos_client)
|
||||
|
||||
def get_target_objects(self, **kwargs):
|
||||
result = self.farmos_client.log.get(self.farmos_log_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:
|
||||
log = self.farmos_client.log.get_id(self.farmos_log_type, str(uuid))
|
||||
except requests.HTTPError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return None
|
||||
raise
|
||||
return log["data"]
|
||||
|
||||
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_log_payload(source_data)
|
||||
result = self.farmos_client.log.send(self.farmos_log_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_log_payload(source_data)
|
||||
payload["id"] = str(source_data["uuid"])
|
||||
result = self.farmos_client.log.send(self.farmos_log_type, payload)
|
||||
return self.normalize_target_object(result["data"])
|
||||
|
||||
def normalize_target_object(self, log):
|
||||
normal = self.normal.normalize_farmos_log(log)
|
||||
return {
|
||||
"uuid": UUID(normal["uuid"]),
|
||||
"name": normal["name"],
|
||||
"timestamp": self.app.make_utc(normal["timestamp"]),
|
||||
"is_movement": normal["is_movement"],
|
||||
"is_group_assignment": normal["is_group_assignment"],
|
||||
"status": normal["status"],
|
||||
"notes": normal["notes"],
|
||||
"quick": normal["quick"],
|
||||
"assets": [(a["asset_type"], UUID(a["uuid"])) for a in normal["assets"]],
|
||||
"quantities": [UUID(uuid) for uuid in normal["quantity_uuids"]],
|
||||
}
|
||||
|
||||
def get_log_payload(self, source_data):
|
||||
|
||||
attrs = {}
|
||||
if "name" in self.fields:
|
||||
attrs["name"] = source_data["name"]
|
||||
if "timestamp" in self.fields:
|
||||
attrs["timestamp"] = self.format_datetime(source_data["timestamp"])
|
||||
if "is_movement" in self.fields:
|
||||
attrs["is_movement"] = source_data["is_movement"]
|
||||
if "is_group_assignment" in self.fields:
|
||||
attrs["is_group_assignment"] = source_data["is_group_assignment"]
|
||||
if "status" in self.fields:
|
||||
attrs["status"] = source_data["status"]
|
||||
if "notes" in self.fields:
|
||||
attrs["notes"] = {"value": source_data["notes"]}
|
||||
if "quick" in self.fields:
|
||||
attrs["quick"] = source_data["quick"]
|
||||
|
||||
rels = {}
|
||||
if "assets" in self.fields:
|
||||
assets = []
|
||||
for asset_type, uuid in source_data["assets"]:
|
||||
assets.append(
|
||||
{
|
||||
"type": f"asset--{asset_type}",
|
||||
"id": str(uuid),
|
||||
}
|
||||
)
|
||||
rels["asset"] = {"data": assets}
|
||||
if "quantities" in self.fields:
|
||||
quantities = []
|
||||
for uuid in source_data["quantities"]:
|
||||
quantities.append(
|
||||
{
|
||||
# TODO: support other quantity types
|
||||
"type": "quantity--standard",
|
||||
"id": str(uuid),
|
||||
}
|
||||
)
|
||||
rels["quantity"] = {"data": quantities}
|
||||
|
||||
payload = {"attributes": attrs, "relationships": rels}
|
||||
return payload
|
||||
|
||||
|
||||
class ActivityLogImporter(ToFarmOSLog):
|
||||
|
||||
model_title = "ActivityLog"
|
||||
farmos_log_type = "activity"
|
||||
|
||||
|
||||
class HarvestLogImporter(ToFarmOSLog):
|
||||
|
||||
model_title = "HarvestLog"
|
||||
farmos_log_type = "harvest"
|
||||
|
||||
|
||||
class MedicalLogImporter(ToFarmOSLog):
|
||||
|
||||
model_title = "MedicalLog"
|
||||
farmos_log_type = "medical"
|
||||
|
||||
def get_supported_fields(self):
|
||||
fields = list(super().get_supported_fields())
|
||||
fields.extend(
|
||||
[
|
||||
"vet",
|
||||
]
|
||||
)
|
||||
return fields
|
||||
|
||||
def normalize_target_object(self, log):
|
||||
data = super().normalize_target_object(log)
|
||||
data.update(
|
||||
{
|
||||
"vet": log["attributes"]["vet"],
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def get_log_payload(self, source_data):
|
||||
payload = super().get_log_payload(source_data)
|
||||
|
||||
if "vet" in self.fields:
|
||||
payload["attributes"]["vet"] = source_data["vet"]
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class ObservationLogImporter(ToFarmOSLog):
|
||||
|
||||
model_title = "ObservationLog"
|
||||
farmos_log_type = "observation"
|
||||
482
src/wuttafarm/farmos/importing/wuttafarm.py
Normal file
482
src/wuttafarm/farmos/importing/wuttafarm.py
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
WuttaFarm → farmOS data export
|
||||
"""
|
||||
|
||||
from oauthlib.oauth2 import BackendApplicationClient
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
from wuttasync.importing import ImportHandler, FromWuttaHandler, FromWutta, Orientation
|
||||
|
||||
from wuttafarm.db import model
|
||||
from wuttafarm.farmos import importing as farmos_importing
|
||||
|
||||
|
||||
class FromWuttaFarmHandler(FromWuttaHandler):
|
||||
"""
|
||||
Base class for import handler targeting WuttaFarm
|
||||
"""
|
||||
|
||||
source_key = "wuttafarm"
|
||||
|
||||
|
||||
class ToFarmOSHandler(ImportHandler):
|
||||
"""
|
||||
Base class for export handlers using CSV file(s) as data target.
|
||||
"""
|
||||
|
||||
target_key = "farmos"
|
||||
generic_target_title = "farmOS"
|
||||
|
||||
# TODO: a lot of duplication to cleanup here; see FromFarmOSHandler
|
||||
|
||||
def begin_target_transaction(self, client=None):
|
||||
"""
|
||||
Establish the farmOS API client.
|
||||
"""
|
||||
if client:
|
||||
self.farmos_client = client
|
||||
else:
|
||||
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)
|
||||
|
||||
def get_farmos_oauth2_token(self):
|
||||
|
||||
client_id = self.config.get(
|
||||
"farmos.oauth2.importing.client_id", default="wuttafarm"
|
||||
)
|
||||
client_secret = self.config.require("farmos.oauth2.importing.client_secret")
|
||||
scope = self.config.get("farmos.oauth2.importing.scope", default="farm_manager")
|
||||
|
||||
client = BackendApplicationClient(client_id=client_id)
|
||||
oauth = OAuth2Session(client=client)
|
||||
|
||||
return oauth.fetch_token(
|
||||
token_url=self.app.get_farmos_url("/oauth/token"),
|
||||
include_client_id=True,
|
||||
client_secret=client_secret,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
def get_importer_kwargs(self, key, **kwargs):
|
||||
kwargs = super().get_importer_kwargs(key, **kwargs)
|
||||
kwargs["farmos_client"] = self.farmos_client
|
||||
kwargs["farmos_4x"] = self.farmos_4x
|
||||
return kwargs
|
||||
|
||||
|
||||
class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
|
||||
"""
|
||||
Handler for WuttaFarm → farmOS API export.
|
||||
"""
|
||||
|
||||
orientation = Orientation.EXPORT
|
||||
|
||||
def define_importers(self):
|
||||
""" """
|
||||
importers = super().define_importers()
|
||||
importers["LandAsset"] = LandAssetImporter
|
||||
importers["StructureAsset"] = StructureAssetImporter
|
||||
importers["AnimalType"] = AnimalTypeImporter
|
||||
importers["AnimalAsset"] = AnimalAssetImporter
|
||||
importers["GroupAsset"] = GroupAssetImporter
|
||||
importers["PlantType"] = PlantTypeImporter
|
||||
importers["PlantAsset"] = PlantAssetImporter
|
||||
importers["Unit"] = UnitImporter
|
||||
importers["StandardQuantity"] = StandardQuantityImporter
|
||||
importers["ActivityLog"] = ActivityLogImporter
|
||||
importers["HarvestLog"] = HarvestLogImporter
|
||||
importers["MedicalLog"] = MedicalLogImporter
|
||||
importers["ObservationLog"] = ObservationLogImporter
|
||||
return importers
|
||||
|
||||
|
||||
class FromWuttaFarm(FromWutta):
|
||||
|
||||
drupal_internal_id_field = "drupal_internal__id"
|
||||
|
||||
def create_target_object(self, key, source_data):
|
||||
obj = super().create_target_object(key, source_data)
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not self.dry_run:
|
||||
|
||||
# set farmOS, Drupal key fields in WuttaFarm
|
||||
api_object = obj["_new_object"]
|
||||
wf_object = source_data["_src_object"]
|
||||
wf_object.farmos_uuid = obj["uuid"]
|
||||
wf_object.drupal_id = api_object["attributes"][
|
||||
self.drupal_internal_id_field
|
||||
]
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class AnimalAssetImporter(FromWuttaFarm, farmos_importing.model.AnimalAssetImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Animal Assets
|
||||
"""
|
||||
|
||||
source_model_class = model.AnimalAsset
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"animal_type_uuid",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"produces_eggs",
|
||||
"birthdate",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_source_object(self, animal):
|
||||
return {
|
||||
"uuid": animal.farmos_uuid or self.app.make_true_uuid(),
|
||||
"asset_name": animal.asset_name,
|
||||
"animal_type_uuid": animal.animal_type.farmos_uuid,
|
||||
"sex": animal.sex,
|
||||
"is_sterile": animal.is_sterile,
|
||||
"produces_eggs": animal.produces_eggs,
|
||||
"birthdate": animal.birthdate,
|
||||
"notes": animal.notes,
|
||||
"archived": animal.archived,
|
||||
"_src_object": animal,
|
||||
}
|
||||
|
||||
|
||||
class AnimalTypeImporter(FromWuttaFarm, farmos_importing.model.AnimalTypeImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Animal Types
|
||||
"""
|
||||
|
||||
source_model_class = model.AnimalType
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
]
|
||||
|
||||
drupal_internal_id_field = "drupal_internal__tid"
|
||||
|
||||
def normalize_source_object(self, animal_type):
|
||||
return {
|
||||
"uuid": animal_type.farmos_uuid or self.app.make_true_uuid(),
|
||||
"name": animal_type.name,
|
||||
"_src_object": animal_type,
|
||||
}
|
||||
|
||||
|
||||
class UnitImporter(FromWuttaFarm, farmos_importing.model.UnitImporter):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
source_model_class = model.GroupAsset
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"produces_eggs",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_source_object(self, group):
|
||||
return {
|
||||
"uuid": group.farmos_uuid or self.app.make_true_uuid(),
|
||||
"asset_name": group.asset_name,
|
||||
"produces_eggs": group.produces_eggs,
|
||||
"notes": group.notes,
|
||||
"archived": group.archived,
|
||||
"_src_object": group,
|
||||
}
|
||||
|
||||
|
||||
class LandAssetImporter(FromWuttaFarm, farmos_importing.model.LandAssetImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Land Assets
|
||||
"""
|
||||
|
||||
source_model_class = model.LandAsset
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"land_type_id",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_source_object(self, land):
|
||||
return {
|
||||
"uuid": land.farmos_uuid or self.app.make_true_uuid(),
|
||||
"asset_name": land.asset_name,
|
||||
"land_type_id": land.land_type.drupal_id,
|
||||
"is_location": land.is_location,
|
||||
"is_fixed": land.is_fixed,
|
||||
"notes": land.notes,
|
||||
"archived": land.archived,
|
||||
"_src_object": land,
|
||||
}
|
||||
|
||||
|
||||
class PlantTypeImporter(FromWuttaFarm, farmos_importing.model.PlantTypeImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Plant Types
|
||||
"""
|
||||
|
||||
source_model_class = model.PlantType
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
]
|
||||
|
||||
drupal_internal_id_field = "drupal_internal__tid"
|
||||
|
||||
def normalize_source_object(self, plant_type):
|
||||
return {
|
||||
"uuid": plant_type.farmos_uuid or self.app.make_true_uuid(),
|
||||
"name": plant_type.name,
|
||||
"_src_object": plant_type,
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Structure Assets
|
||||
"""
|
||||
|
||||
source_model_class = model.StructureAsset
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"asset_name",
|
||||
"structure_type_id",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def normalize_source_object(self, structure):
|
||||
return {
|
||||
"uuid": structure.farmos_uuid or self.app.make_true_uuid(),
|
||||
"asset_name": structure.asset_name,
|
||||
"structure_type_id": structure.structure_type.drupal_id,
|
||||
"is_location": structure.is_location,
|
||||
"is_fixed": structure.is_fixed,
|
||||
"notes": structure.notes,
|
||||
"archived": structure.archived,
|
||||
"_src_object": structure,
|
||||
}
|
||||
|
||||
|
||||
##############################
|
||||
# quantity importers
|
||||
##############################
|
||||
|
||||
|
||||
class FromWuttaFarmQuantity(FromWuttaFarm):
|
||||
"""
|
||||
Base class for WuttaFarm -> farmOS quantity importers
|
||||
"""
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"measure",
|
||||
"value_numerator",
|
||||
"value_denominator",
|
||||
"label",
|
||||
"quantity_type_uuid",
|
||||
"unit_uuid",
|
||||
]
|
||||
|
||||
def normalize_source_object(self, qty):
|
||||
return {
|
||||
"uuid": qty.farmos_uuid or self.app.make_true_uuid(),
|
||||
"measure": qty.measure_id,
|
||||
"value_numerator": qty.value_numerator,
|
||||
"value_denominator": qty.value_denominator,
|
||||
"label": qty.label,
|
||||
"quantity_type_uuid": qty.quantity_type.farmos_uuid,
|
||||
"unit_uuid": qty.units.farmos_uuid,
|
||||
"_src_object": qty,
|
||||
}
|
||||
|
||||
|
||||
class StandardQuantityImporter(
|
||||
FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter
|
||||
):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Standard Quantities
|
||||
"""
|
||||
|
||||
source_model_class = model.StandardQuantity
|
||||
|
||||
|
||||
##############################
|
||||
# log importers
|
||||
##############################
|
||||
|
||||
|
||||
class FromWuttaFarmLog(FromWuttaFarm):
|
||||
"""
|
||||
Base class for WuttaFarm -> farmOS log importers
|
||||
"""
|
||||
|
||||
supported_fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
"timestamp",
|
||||
"is_movement",
|
||||
"is_group_assignment",
|
||||
"status",
|
||||
"notes",
|
||||
"quick",
|
||||
"assets",
|
||||
"quantities",
|
||||
]
|
||||
|
||||
def normalize_source_object(self, log):
|
||||
return {
|
||||
"uuid": log.farmos_uuid or self.app.make_true_uuid(),
|
||||
"name": log.message,
|
||||
"timestamp": log.timestamp,
|
||||
"is_movement": log.is_movement,
|
||||
"is_group_assignment": log.is_group_assignment,
|
||||
"status": log.status,
|
||||
"notes": log.notes,
|
||||
"quick": self.config.parse_list(log.quick) if log.quick else [],
|
||||
"assets": [(a.asset_type, a.farmos_uuid) for a in log.assets],
|
||||
"quantities": [qty.farmos_uuid for qty in log.quantities],
|
||||
"_src_object": log,
|
||||
}
|
||||
|
||||
|
||||
class ActivityLogImporter(FromWuttaFarmLog, farmos_importing.model.ActivityLogImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Activity Logs
|
||||
"""
|
||||
|
||||
source_model_class = model.ActivityLog
|
||||
|
||||
|
||||
class HarvestLogImporter(FromWuttaFarmLog, farmos_importing.model.HarvestLogImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Harvest Logs
|
||||
"""
|
||||
|
||||
source_model_class = model.HarvestLog
|
||||
|
||||
|
||||
class MedicalLogImporter(FromWuttaFarmLog, farmos_importing.model.MedicalLogImporter):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Medical Logs
|
||||
"""
|
||||
|
||||
source_model_class = model.MedicalLog
|
||||
|
||||
def get_supported_fields(self):
|
||||
fields = list(super().get_supported_fields())
|
||||
fields.extend(
|
||||
[
|
||||
"vet",
|
||||
]
|
||||
)
|
||||
return fields
|
||||
|
||||
def normalize_source_object(self, log):
|
||||
data = super().normalize_source_object(log)
|
||||
data.update(
|
||||
{
|
||||
"vet": log.vet,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class ObservationLogImporter(
|
||||
FromWuttaFarmLog, farmos_importing.model.ObservationLogImporter
|
||||
):
|
||||
"""
|
||||
WuttaFarm → farmOS API exporter for Observation Logs
|
||||
"""
|
||||
|
||||
source_model_class = model.ObservationLog
|
||||
File diff suppressed because it is too large
Load diff
292
src/wuttafarm/normal.py
Normal file
292
src/wuttafarm/normal.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
# -*- 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_asset(self, asset, included={}):
|
||||
""" """
|
||||
|
||||
if notes := asset["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
|
||||
owner_objects = []
|
||||
owner_uuids = []
|
||||
if relationships := asset.get("relationships"):
|
||||
|
||||
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": asset["id"],
|
||||
"drupal_id": asset["attributes"]["drupal_internal__id"],
|
||||
"asset_name": asset["attributes"]["name"],
|
||||
"is_location": asset["attributes"]["is_location"],
|
||||
"is_fixed": asset["attributes"]["is_fixed"],
|
||||
"archived": asset["attributes"]["archived"],
|
||||
"notes": notes,
|
||||
"owners": owner_objects,
|
||||
"owner_uuids": owner_uuids,
|
||||
}
|
||||
|
||||
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 = []
|
||||
group_objects = []
|
||||
group_uuids = []
|
||||
quantity_objects = []
|
||||
quantity_uuids = []
|
||||
location_objects = []
|
||||
location_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 groups := relationships.get("group"):
|
||||
for group in groups["data"]:
|
||||
group_uuid = group["id"]
|
||||
group_uuids.append(group_uuid)
|
||||
group_object = {
|
||||
"uuid": group["id"],
|
||||
"type": group["type"],
|
||||
"asset_type": group["type"].split("--")[1],
|
||||
}
|
||||
if group := included.get(group_uuid):
|
||||
attrs = group["attributes"]
|
||||
rels = group["relationships"]
|
||||
group_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"],
|
||||
}
|
||||
)
|
||||
group_objects.append(group_object)
|
||||
|
||||
if locations := relationships.get("location"):
|
||||
for location in locations["data"]:
|
||||
location_uuid = location["id"]
|
||||
location_uuids.append(location_uuid)
|
||||
location_object = {
|
||||
"uuid": location["id"],
|
||||
"type": location["type"],
|
||||
"asset_type": location["type"].split("--")[1],
|
||||
}
|
||||
if location := included.get(location_uuid):
|
||||
attrs = location["attributes"]
|
||||
rels = location["relationships"]
|
||||
location_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"],
|
||||
}
|
||||
)
|
||||
location_objects.append(location_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,
|
||||
"groups": group_objects,
|
||||
"group_uuids": group_uuids,
|
||||
"quantities": quantity_objects,
|
||||
"quantity_uuids": quantity_uuids,
|
||||
"is_group_assignment": log["attributes"]["is_group_assignment"],
|
||||
"is_movement": log["attributes"]["is_movement"],
|
||||
"quick": log["attributes"]["quick"],
|
||||
"status": log["attributes"]["status"],
|
||||
"notes": notes,
|
||||
"locations": location_objects,
|
||||
"location_uuids": location_uuids,
|
||||
"owners": owner_objects,
|
||||
"owner_uuids": owner_uuids,
|
||||
# TODO: should we do this here or make caller do it?
|
||||
"vet": log["attributes"].get("vet"),
|
||||
}
|
||||
37
src/wuttafarm/util.py
Normal file
37
src/wuttafarm/util.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
misc. utilities
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def get_log_type_enum(config, session=None):
|
||||
app = config.get_app()
|
||||
model = app.model
|
||||
log_types = OrderedDict()
|
||||
with app.short_session(session=session) as sess:
|
||||
query = sess.query(model.LogType).order_by(model.LogType.name)
|
||||
for log_type in query:
|
||||
log_types[log_type.drupal_id] = log_type.name
|
||||
return log_types
|
||||
|
|
@ -40,6 +40,15 @@ def main(global_config, **settings):
|
|||
"wuttaweb:templates",
|
||||
],
|
||||
)
|
||||
settings.setdefault(
|
||||
"pyramid_deform.template_search_path",
|
||||
" ".join(
|
||||
[
|
||||
"wuttafarm.web:templates/deform",
|
||||
"wuttaweb:templates/deform",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# make config objects
|
||||
wutta_config = base.make_wutta_config(settings)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ import json
|
|||
|
||||
import colander
|
||||
|
||||
from wuttaweb.forms.schema import ObjectRef
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.forms.schema import ObjectRef, WuttaSet
|
||||
from wuttaweb.forms.widgets import NotesWidget
|
||||
|
||||
|
||||
class AnimalTypeRef(ObjectRef):
|
||||
|
|
@ -54,8 +56,168 @@ class AnimalTypeRef(ObjectRef):
|
|||
animal_type = obj
|
||||
return self.request.route_url("animal_types.view", uuid=animal_type.uuid)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import AnimalTypeRefWidget
|
||||
|
||||
class AnimalTypeType(colander.SchemaType):
|
||||
kwargs["factory"] = AnimalTypeRefWidget
|
||||
return super().widget_maker(**kwargs)
|
||||
|
||||
|
||||
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 LogRef(ObjectRef):
|
||||
"""
|
||||
Custom schema type for a
|
||||
:class:`~wuttafarm.db.model.log.Log` reference field.
|
||||
|
||||
This is a subclass of
|
||||
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||
"""
|
||||
|
||||
@property
|
||||
def model_class(self): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
model = self.app.model
|
||||
return model.Log
|
||||
|
||||
def sort_query(self, query): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
return query.order_by(self.model_class.message)
|
||||
|
||||
def get_object_url(self, obj): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
log = obj
|
||||
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
||||
|
||||
|
||||
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 FarmOSPlantTypes(colander.SchemaType):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
@ -69,9 +231,9 @@ class AnimalTypeType(colander.SchemaType):
|
|||
|
||||
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
from wuttafarm.web.forms.widgets import AnimalTypeWidget
|
||||
from wuttafarm.web.forms.widgets import FarmOSPlantTypesWidget
|
||||
|
||||
return AnimalTypeWidget(self.request, **kwargs)
|
||||
return FarmOSPlantTypesWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class LandTypeRef(ObjectRef):
|
||||
|
|
@ -99,6 +261,33 @@ class LandTypeRef(ObjectRef):
|
|||
return self.request.route_url("land_types.view", uuid=land_type.uuid)
|
||||
|
||||
|
||||
class PlantTypeRefs(WuttaSet):
|
||||
"""
|
||||
Schema type for Plant Types field (on a Plant Asset).
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
return [uuid.hex for uuid in appstruct]
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import PlantTypeRefsWidget
|
||||
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
if "values" not in kwargs:
|
||||
plant_types = (
|
||||
session.query(model.PlantType).order_by(model.PlantType.name).all()
|
||||
)
|
||||
values = [(pt.uuid.hex, str(pt)) for pt in plant_types]
|
||||
kwargs["values"] = values
|
||||
|
||||
return PlantTypeRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class StructureType(colander.SchemaType):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
|
|
@ -143,6 +332,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):
|
||||
|
|
@ -160,3 +370,93 @@ class UsersType(colander.SchemaType):
|
|||
from wuttafarm.web.forms.widgets import UsersWidget
|
||||
|
||||
return UsersWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class AssetParentRefs(WuttaSet):
|
||||
"""
|
||||
Schema type for Parents field which references assets.
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
appstruct = []
|
||||
uuids = [u.hex for u in appstruct]
|
||||
return json.dumps(uuids)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import AssetParentRefsWidget
|
||||
|
||||
return AssetParentRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class AssetRefs(WuttaSet):
|
||||
"""
|
||||
Schema type for Assets field (on a Log record)
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
return {asset.uuid for asset in appstruct}
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import AssetRefsWidget
|
||||
|
||||
return AssetRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class LogQuantityRefs(WuttaSet):
|
||||
"""
|
||||
Schema type for Quantities field (on a Log record)
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
return {qty.uuid for qty in appstruct}
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import LogQuantityRefsWidget
|
||||
|
||||
return LogQuantityRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class OwnerRefs(WuttaSet):
|
||||
"""
|
||||
Schema type for Owners field (on a Log record)
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
return {user.uuid for user in appstruct}
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import OwnerRefsWidget
|
||||
|
||||
return OwnerRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class Notes(colander.String):
|
||||
"""
|
||||
Custom schema type for "note" fields.
|
||||
"""
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
""" """
|
||||
if not appstruct:
|
||||
return colander.null
|
||||
|
||||
return super().serialize(node, appstruct)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
"""
|
||||
Construct a default widget for the field.
|
||||
|
||||
:returns: Instance of
|
||||
:class:`~wuttaweb.forms.widgets.NotesWidget`.
|
||||
"""
|
||||
return NotesWidget(**kwargs)
|
||||
|
|
|
|||
|
|
@ -26,9 +26,14 @@ Custom form widgets for WuttaFarm
|
|||
import json
|
||||
|
||||
import colander
|
||||
from deform.widget import Widget
|
||||
from deform.widget import Widget, SelectWidget, sequence_types, _normalize_choices
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
||||
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget
|
||||
from wuttaweb.db import Session
|
||||
|
||||
from wuttafarm.web.util import render_quantity_objects
|
||||
|
||||
|
||||
class ImageWidget(Widget):
|
||||
"""
|
||||
|
|
@ -51,9 +56,88 @@ class ImageWidget(Widget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class AnimalTypeWidget(Widget):
|
||||
class LogQuickWidget(Widget):
|
||||
"""
|
||||
Widget to display an "animal type" field.
|
||||
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):
|
||||
|
|
@ -67,17 +151,187 @@ class AnimalTypeWidget(Widget):
|
|||
if cstruct in (colander.null, None):
|
||||
return HTML.tag("span")
|
||||
|
||||
animal_type = json.loads(cstruct)
|
||||
return tags.link_to(
|
||||
animal_type["name"],
|
||||
self.request.route_url(
|
||||
"farmos_animal_types.view", uuid=animal_type["uuid"]
|
||||
),
|
||||
)
|
||||
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 FarmOSPlantTypesWidget(Widget):
|
||||
"""
|
||||
Widget to display a farmOS "plant types" field.
|
||||
"""
|
||||
|
||||
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")
|
||||
|
||||
links = []
|
||||
for plant_type in json.loads(cstruct):
|
||||
link = tags.link_to(
|
||||
plant_type["name"],
|
||||
self.request.route_url(
|
||||
"farmos_plant_types.view", uuid=plant_type["uuid"]
|
||||
),
|
||||
)
|
||||
links.append(HTML.tag("li", c=link))
|
||||
return HTML.tag("ul", c=links)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class PlantTypeRefsWidget(Widget):
|
||||
"""
|
||||
Widget for Plant Types field (on a Plant Asset).
|
||||
"""
|
||||
|
||||
template = "planttyperefs"
|
||||
values = ()
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
if cstruct in (colander.null, None):
|
||||
cstruct = ()
|
||||
|
||||
if readonly := kw.get("readonly", self.readonly):
|
||||
items = []
|
||||
|
||||
plant_types = (
|
||||
session.query(model.PlantType)
|
||||
.filter(model.PlantType.uuid.in_(cstruct))
|
||||
.order_by(model.PlantType.name)
|
||||
.all()
|
||||
)
|
||||
|
||||
for plant_type in plant_types:
|
||||
items.append(
|
||||
HTML.tag(
|
||||
"li",
|
||||
c=tags.link_to(
|
||||
str(plant_type),
|
||||
self.request.route_url(
|
||||
"plant_types.view", uuid=plant_type.uuid
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return HTML.tag("ul", c=items)
|
||||
|
||||
values = kw.get("values", self.values)
|
||||
if not isinstance(values, sequence_types):
|
||||
raise TypeError("Values must be a sequence type (list, tuple, or range).")
|
||||
|
||||
kw["values"] = _normalize_choices(values)
|
||||
tmpl_values = self.get_template_values(field, cstruct, kw)
|
||||
return field.renderer(self.template, **tmpl_values)
|
||||
|
||||
def get_template_values(self, field, cstruct, kw):
|
||||
""" """
|
||||
values = super().get_template_values(field, cstruct, kw)
|
||||
|
||||
values["js_values"] = json.dumps(values["values"])
|
||||
|
||||
if self.request.has_perm("plant_types.create"):
|
||||
values["can_create"] = True
|
||||
|
||||
return values
|
||||
|
||||
def deserialize(self, field, pstruct):
|
||||
""" """
|
||||
if not pstruct:
|
||||
return colander.null
|
||||
|
||||
return set(pstruct.split(","))
|
||||
|
||||
|
||||
class StructureWidget(Widget):
|
||||
"""
|
||||
Widget to display a "structure" field.
|
||||
|
|
@ -98,7 +352,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"]
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -132,3 +386,152 @@ class UsersWidget(Widget):
|
|||
return HTML.tag("ul", c=items)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
##############################
|
||||
# native data widgets
|
||||
##############################
|
||||
|
||||
|
||||
class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
|
||||
"""
|
||||
Widget for Parents field which references assets.
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
parents = []
|
||||
for uuid in json.loads(cstruct):
|
||||
parent = session.get(model.Asset, uuid)
|
||||
parents.append(
|
||||
HTML.tag(
|
||||
"li",
|
||||
c=tags.link_to(
|
||||
str(parent),
|
||||
self.request.route_url(
|
||||
f"{parent.asset_type}_assets.view", uuid=parent.uuid
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return HTML.tag("ul", c=parents)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class AssetRefsWidget(WuttaCheckboxChoiceWidget):
|
||||
"""
|
||||
Widget for Assets field (of various kinds).
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
assets = []
|
||||
for uuid in cstruct or []:
|
||||
asset = session.get(model.Asset, uuid)
|
||||
assets.append(
|
||||
HTML.tag(
|
||||
"li",
|
||||
c=tags.link_to(
|
||||
str(asset),
|
||||
self.request.route_url(
|
||||
f"{asset.asset_type}_assets.view", uuid=asset.uuid
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return HTML.tag("ul", c=assets)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class LogQuantityRefsWidget(WuttaCheckboxChoiceWidget):
|
||||
"""
|
||||
Widget for Quantities field (on a Log record)
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
quantities = []
|
||||
for uuid in cstruct or []:
|
||||
qty = session.get(model.Quantity, uuid)
|
||||
quantities.append(
|
||||
HTML.tag(
|
||||
"li",
|
||||
c=tags.link_to(
|
||||
qty.render_as_text(self.config),
|
||||
# TODO
|
||||
self.request.route_url(
|
||||
"quantities_standard.view", uuid=qty.uuid
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return HTML.tag("ul", c=quantities)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class OwnerRefsWidget(WuttaCheckboxChoiceWidget):
|
||||
"""
|
||||
Widget for Owners field (on an Asset or Log record)
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
model = self.app.model
|
||||
session = Session()
|
||||
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
owners = [session.get(model.User, uuid) for uuid in cstruct or []]
|
||||
owners = [user for user in owners if user]
|
||||
owners.sort(key=lambda user: user.username)
|
||||
links = []
|
||||
for user in owners:
|
||||
links.append(
|
||||
HTML.tag(
|
||||
"li",
|
||||
c=tags.link_to(
|
||||
user.username,
|
||||
self.request.route_url("users.view", uuid=user.uuid),
|
||||
),
|
||||
)
|
||||
)
|
||||
return HTML.tag("ul", c=links)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class AnimalTypeRefWidget(ObjectRefWidget):
|
||||
"""
|
||||
Custom widget which uses the ``<animal-type-picker>`` component.
|
||||
"""
|
||||
|
||||
template = "animaltyperef"
|
||||
|
||||
def get_template_values(self, field, cstruct, kw):
|
||||
""" """
|
||||
values = super().get_template_values(field, cstruct, kw)
|
||||
|
||||
values["js_values"] = json.dumps(values["values"])
|
||||
|
||||
if self.request.has_perm("animal_types.create"):
|
||||
values["can_create"] = True
|
||||
|
||||
return values
|
||||
|
|
|
|||
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": "quick.eggs",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def make_asset_menu(self, request):
|
||||
return {
|
||||
|
|
@ -45,41 +83,56 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"type": "menu",
|
||||
"items": [
|
||||
{
|
||||
"title": "Animals",
|
||||
"route": "animals",
|
||||
"perm": "animals.list",
|
||||
"title": "All Assets",
|
||||
"route": "assets",
|
||||
"perm": "assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Groups",
|
||||
"route": "groups",
|
||||
"perm": "groups.list",
|
||||
"title": "Animal",
|
||||
"route": "animal_assets",
|
||||
"perm": "animal_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Structures",
|
||||
"route": "structures",
|
||||
"perm": "structures.list",
|
||||
"title": "Group",
|
||||
"route": "group_assets",
|
||||
"perm": "group_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Land",
|
||||
"route": "land_assets",
|
||||
"perm": "land_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Plant",
|
||||
"route": "plant_assets",
|
||||
"perm": "plant_assets.list",
|
||||
},
|
||||
{
|
||||
"title": "Structure",
|
||||
"route": "structure_assets",
|
||||
"perm": "structure_assets.list",
|
||||
},
|
||||
{"type": "sep"},
|
||||
{
|
||||
"title": "Animal Types",
|
||||
"route": "animal_types",
|
||||
"perm": "animal_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Structure Types",
|
||||
"route": "structure_types",
|
||||
"perm": "structure_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Land Types",
|
||||
"route": "land_types",
|
||||
"perm": "land_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Plant Types",
|
||||
"route": "plant_types",
|
||||
"perm": "plant_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Structure Types",
|
||||
"route": "structure_types",
|
||||
"perm": "structure_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Asset Types",
|
||||
"route": "asset_types",
|
||||
|
|
@ -94,9 +147,40 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"type": "menu",
|
||||
"items": [
|
||||
{
|
||||
"title": "Activity Logs",
|
||||
"route": "activity_logs",
|
||||
"perm": "activity_logs.list",
|
||||
"title": "All Logs",
|
||||
"route": "log",
|
||||
"perm": "log.list",
|
||||
},
|
||||
{
|
||||
"title": "Activity",
|
||||
"route": "logs_activity",
|
||||
"perm": "logs_activity.list",
|
||||
},
|
||||
{
|
||||
"title": "Harvest",
|
||||
"route": "logs_harvest",
|
||||
"perm": "logs_harvest.list",
|
||||
},
|
||||
{
|
||||
"title": "Medical",
|
||||
"route": "logs_medical",
|
||||
"perm": "logs_medical.list",
|
||||
},
|
||||
{
|
||||
"title": "Observation",
|
||||
"route": "logs_observation",
|
||||
"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"},
|
||||
{
|
||||
|
|
@ -104,10 +188,25 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
"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 {
|
||||
|
|
@ -121,47 +220,73 @@ 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": "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",
|
||||
"route": "farmos_logs_activity",
|
||||
"perm": "farmos_logs_activity.list",
|
||||
},
|
||||
{
|
||||
"title": "Harvest Logs",
|
||||
"route": "farmos_logs_harvest",
|
||||
"perm": "farmos_logs_harvest.list",
|
||||
},
|
||||
{
|
||||
"title": "Medical Logs",
|
||||
"route": "farmos_logs_medical",
|
||||
"perm": "farmos_logs_medical.list",
|
||||
},
|
||||
{
|
||||
"title": "Observation Logs",
|
||||
"route": "farmos_logs_observation",
|
||||
"perm": "farmos_logs_observation.list",
|
||||
},
|
||||
{"type": "sep"},
|
||||
{
|
||||
"title": "Animal Types",
|
||||
"route": "farmos_animal_types",
|
||||
"perm": "farmos_animal_types.list",
|
||||
},
|
||||
{
|
||||
"title": "Structure Types",
|
||||
"route": "farmos_structure_types",
|
||||
"perm": "farmos_structure_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",
|
||||
|
|
@ -172,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",
|
||||
|
|
|
|||
82
src/wuttafarm/web/templates/appinfo/configure.mako
Normal file
82
src/wuttafarm/web/templates/appinfo/configure.mako
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
## -*- 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 grouped>
|
||||
|
||||
<b-field label="OAuth2 Client ID">
|
||||
<b-input name="farmos.oauth2.client_id"
|
||||
v-model="simpleSettings['farmos.oauth2.client_id']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="OAuth2 Scope">
|
||||
<b-input name="farmos.oauth2.scope"
|
||||
v-model="simpleSettings['farmos.oauth2.scope']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
<b-field label="OAuth2 Redirect URI">
|
||||
<wutta-copyable-text text="${url('farmos_oauth_callback')}" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="farmOS Integration Mode">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<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}-tooltip position="${'right' if request.use_oruga else 'is-right'}">
|
||||
<b-icon pack="fas" icon="info-circle" type="is-warning" />
|
||||
<template #content>
|
||||
<p class="block">
|
||||
<span class="has-text-weight-bold">RESTART IS REQUIRED</span>
|
||||
if you change the integration mode.
|
||||
</p>
|
||||
</template>
|
||||
</${b}-tooltip>
|
||||
</div>
|
||||
</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/assets/master/view.mako
Normal file
14
src/wuttafarm/web/templates/assets/master/view.mako
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="page_content()">
|
||||
|
||||
% if instance.archived:
|
||||
<b-notification type="is-warning">
|
||||
This asset is archived.
|
||||
Archived assets should only be edited if they need corrections.
|
||||
</b-notification>
|
||||
% endif
|
||||
|
||||
${parent.page_content()}
|
||||
</%def>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<%inherit file="wuttaweb:templates/base.mako" />
|
||||
<%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" />
|
||||
|
||||
<%def name="index_title_controls()">
|
||||
${parent.index_title_controls()}
|
||||
|
|
@ -14,3 +15,8 @@
|
|||
% endif
|
||||
|
||||
</%def>
|
||||
|
||||
<%def name="render_vue_templates()">
|
||||
${parent.render_vue_templates()}
|
||||
${make_wuttafarm_components()}
|
||||
</%def>
|
||||
|
|
|
|||
|
|
@ -12,5 +12,10 @@
|
|||
</%def>
|
||||
|
||||
<%def name="footer()">
|
||||
${parent.footer()}
|
||||
<p class="has-text-centered">
|
||||
powered by
|
||||
${h.link_to("WuttaWeb", 'https://wuttaproject.org/', target='_blank')}
|
||||
and
|
||||
${h.link_to("farmOS", 'https://farmos.org/', target='_blank')}
|
||||
</p>
|
||||
</%def>
|
||||
|
|
|
|||
13
src/wuttafarm/web/templates/deform/animaltyperef.pt
Normal file
13
src/wuttafarm/web/templates/deform/animaltyperef.pt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div tal:define="
|
||||
name name|field.name;
|
||||
oid oid|field.oid;
|
||||
vmodel vmodel|'modelData.'+oid;
|
||||
can_create can_create|False;"
|
||||
tal:omit-tag="">
|
||||
|
||||
<animal-type-picker tal:attributes="name name;
|
||||
v-model vmodel;
|
||||
:animal-types js_values;
|
||||
:can-create str(can_create).lower();" />
|
||||
|
||||
</div>
|
||||
13
src/wuttafarm/web/templates/deform/planttyperefs.pt
Normal file
13
src/wuttafarm/web/templates/deform/planttyperefs.pt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div tal:define="
|
||||
name name|field.name;
|
||||
oid oid|field.oid;
|
||||
vmodel vmodel|'modelData.'+oid;
|
||||
can_create can_create|False;"
|
||||
tal:omit-tag="">
|
||||
|
||||
<plant-types-picker tal:attributes="name name;
|
||||
v-model vmodel;
|
||||
:plant-types js_values;
|
||||
:can-create str(can_create).lower();" />
|
||||
|
||||
</div>
|
||||
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>
|
||||
324
src/wuttafarm/web/templates/wuttafarm-components.mako
Normal file
324
src/wuttafarm/web/templates/wuttafarm-components.mako
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
|
||||
<%def name="make_wuttafarm_components()">
|
||||
${self.make_animal_type_picker_component()}
|
||||
${self.make_plant_types_picker_component()}
|
||||
</%def>
|
||||
|
||||
<%def name="make_animal_type_picker_component()">
|
||||
<script type="text/x-template" id="animal-type-picker-template">
|
||||
<div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
|
||||
<b-select :name="name"
|
||||
:value="internalValue"
|
||||
@input="val => $emit('input', val)"
|
||||
style="flex-grow: 1;">
|
||||
<option v-for="atype in internalAnimalTypes"
|
||||
:value="atype[0]">
|
||||
{{ atype[1] }}
|
||||
</option>
|
||||
</b-select>
|
||||
|
||||
<b-button v-if="canCreate"
|
||||
type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="createInit()">
|
||||
New
|
||||
</b-button>
|
||||
|
||||
</div>
|
||||
<${b}-modal v-if="canCreate"
|
||||
has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="createShowDialog"
|
||||
% else:
|
||||
:active.sync="createShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">New Animal Type</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<b-field label="Name" horizontal>
|
||||
<b-input v-model="createName"
|
||||
ref="createName"
|
||||
expanded
|
||||
@keydown.native="createNameKeydown" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="createSave()"
|
||||
:disabled="createSaving || !createName"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ createSaving ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="createShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
const AnimalTypePicker = {
|
||||
template: '#animal-type-picker-template',
|
||||
mixins: [WuttaRequestMixin],
|
||||
props: {
|
||||
name: String,
|
||||
value: String,
|
||||
animalTypes: Array,
|
||||
canCreate: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalAnimalTypes: this.animalTypes,
|
||||
internalValue: this.value,
|
||||
createShowDialog: false,
|
||||
createName: null,
|
||||
createSaving: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
createInit(name) {
|
||||
this.createName = name || null
|
||||
this.createShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.createName.focus()
|
||||
})
|
||||
},
|
||||
|
||||
createNameKeydown(event) {
|
||||
// nb. must prevent main form submit on ENTER
|
||||
// (since ultimately this lives within an outer form)
|
||||
// but also we can submit the modal pseudo-form
|
||||
if (event.which == 13) {
|
||||
event.preventDefault()
|
||||
this.createSave()
|
||||
}
|
||||
},
|
||||
|
||||
createSave() {
|
||||
this.createSaving = true
|
||||
const url = "${url('animal_types.ajax_create')}"
|
||||
const params = {name: this.createName}
|
||||
this.wuttaPOST(url, params, response => {
|
||||
this.internalAnimalTypes.push([response.data.uuid, response.data.name])
|
||||
this.$nextTick(() => {
|
||||
this.internalValue = response.data.uuid
|
||||
this.createSaving = false
|
||||
this.createShowDialog = false
|
||||
})
|
||||
}, response => {
|
||||
this.createSaving = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
Vue.component('animal-type-picker', AnimalTypePicker)
|
||||
<% request.register_component('animal-type-picker', 'AnimalTypePicker') %>
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="make_plant_types_picker_component()">
|
||||
<script type="text/x-template" id="plant-types-picker-template">
|
||||
<div>
|
||||
<input type="hidden" :name="name" :value="value" />
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
|
||||
<span>Add:</span>
|
||||
|
||||
<b-autocomplete v-model="addName"
|
||||
ref="addName"
|
||||
:data="addNameData"
|
||||
field="name"
|
||||
open-on-focus
|
||||
keep-first
|
||||
@select="addNameSelected"
|
||||
clear-on-select
|
||||
style="flex-grow: 1;">
|
||||
<template #empty>No results found</template>
|
||||
</b-autocomplete>
|
||||
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="createInit()">
|
||||
New
|
||||
</b-button>
|
||||
|
||||
</div>
|
||||
|
||||
<${b}-table :data="plantTypeData">
|
||||
|
||||
<${b}-table-column field="name" v-slot="props">
|
||||
<span>{{ props.row.name }}</span>
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column v-slot="props">
|
||||
<a href="#"
|
||||
class="has-text-danger"
|
||||
@click.prevent="removePlantType(props.row)">
|
||||
<i class="fas fa-trash" /> Remove
|
||||
</a>
|
||||
</${b}-table-column>
|
||||
|
||||
</${b}-table>
|
||||
|
||||
<${b}-modal v-if="canCreate"
|
||||
has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="createShowDialog"
|
||||
% else:
|
||||
:active.sync="createShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">New Plant Type</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<b-field label="Name" horizontal>
|
||||
<b-input v-model="createName"
|
||||
ref="createName"
|
||||
expanded
|
||||
@keydown.native="createNameKeydown" />
|
||||
</b-field>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="createSave()"
|
||||
:disabled="createSaving || !createName"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ createSaving ? "Working, please wait..." : "Save" }}
|
||||
</b-button>
|
||||
<b-button @click="createShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
const PlantTypesPicker = {
|
||||
template: '#plant-types-picker-template',
|
||||
mixins: [WuttaRequestMixin],
|
||||
props: {
|
||||
name: String,
|
||||
value: Array,
|
||||
plantTypes: Array,
|
||||
canCreate: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalPlantTypes: this.plantTypes.map((pt) => {
|
||||
return {uuid: pt[0], name: pt[1]}
|
||||
}),
|
||||
|
||||
addShowDialog: false,
|
||||
addName: '',
|
||||
|
||||
createShowDialog: false,
|
||||
createName: null,
|
||||
createSaving: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
plantTypeData() {
|
||||
const data = []
|
||||
|
||||
if (this.value) {
|
||||
for (let ptype of this.internalPlantTypes) {
|
||||
// ptype = {uuid: ptype[0], name: ptype[1]}
|
||||
if (this.value.includes(ptype.uuid)) {
|
||||
data.push(ptype)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
addNameData() {
|
||||
if (!this.addName) {
|
||||
return this.internalPlantTypes
|
||||
}
|
||||
|
||||
return this.internalPlantTypes.filter((ptype) => {
|
||||
return ptype.name.toLowerCase().indexOf(this.addName.toLowerCase()) >= 0
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
addNameSelected(option) {
|
||||
const value = Array.from(this.value || [])
|
||||
|
||||
if (!value.includes(option.uuid)) {
|
||||
value.push(option.uuid)
|
||||
this.$emit('input', value)
|
||||
}
|
||||
|
||||
this.addName = null
|
||||
},
|
||||
|
||||
createInit() {
|
||||
this.createName = this.addName
|
||||
this.createShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.createName.focus()
|
||||
})
|
||||
},
|
||||
|
||||
createNameKeydown(event) {
|
||||
// nb. must prevent main form submit on ENTER
|
||||
// (since ultimately this lives within an outer form)
|
||||
// but also we can submit the modal pseudo-form
|
||||
if (event.which == 13) {
|
||||
event.preventDefault()
|
||||
this.createSave()
|
||||
}
|
||||
},
|
||||
|
||||
createSave() {
|
||||
this.createSaving = true
|
||||
const url = "${url('plant_types.ajax_create')}"
|
||||
const params = {name: this.createName}
|
||||
this.wuttaPOST(url, params, response => {
|
||||
this.internalPlantTypes.push(response.data)
|
||||
const value = Array.from(this.value || [])
|
||||
value.push(response.data.uuid)
|
||||
this.$emit('input', value)
|
||||
this.addName = null
|
||||
this.createSaving = false
|
||||
this.createShowDialog = false
|
||||
}, response => {
|
||||
this.createSaving = false
|
||||
})
|
||||
},
|
||||
|
||||
removePlantType(ptype) {
|
||||
let value = Array.from(this.value)
|
||||
const i = value.indexOf(ptype.uuid)
|
||||
value.splice(i, 1)
|
||||
this.$emit('input', value)
|
||||
},
|
||||
},
|
||||
}
|
||||
Vue.component('plant-types-picker', PlantTypesPicker)
|
||||
<% request.register_component('plant-types-picker', 'PlantTypesPicker') %>
|
||||
</script>
|
||||
</%def>
|
||||
|
|
@ -23,6 +23,28 @@
|
|||
Misc. utilities for web app
|
||||
"""
|
||||
|
||||
from pyramid import httpexceptions
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
|
||||
def get_farmos_client_for_user(request):
|
||||
token = request.session.get("farmos.oauth2.token")
|
||||
if not token:
|
||||
raise httpexceptions.HTTPForbidden()
|
||||
|
||||
# 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(request, token)
|
||||
|
||||
config = request.wutta_config
|
||||
app = config.get_app()
|
||||
return app.get_farmos_client(token=token, token_updater=token_updater)
|
||||
|
||||
|
||||
def save_farmos_oauth2_token(request, token):
|
||||
"""
|
||||
|
|
@ -38,3 +60,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,21 +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.land_types")
|
||||
config.include("wuttafarm.web.views.structure_types")
|
||||
config.include("wuttafarm.web.views.animal_types")
|
||||
config.include("wuttafarm.web.views.land_assets")
|
||||
config.include("wuttafarm.web.views.structures")
|
||||
config.include("wuttafarm.web.views.animals")
|
||||
config.include("wuttafarm.web.views.groups")
|
||||
config.include("wuttafarm.web.views.log_types")
|
||||
config.include("wuttafarm.web.views.logs_activity")
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Master view for Animal Types
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.animals import AnimalType, Animal
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
|
||||
|
||||
class AnimalTypeView(WuttaFarmMasterView):
|
||||
"""
|
||||
Master view for Animal Types
|
||||
"""
|
||||
|
||||
model_class = AnimalType
|
||||
route_prefix = "animal_types"
|
||||
url_prefix = "/animal-types"
|
||||
|
||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"description",
|
||||
"changed",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"description",
|
||||
"changed",
|
||||
"farmos_uuid",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
has_rows = True
|
||||
row_model_class = Animal
|
||||
rows_viewable = True
|
||||
|
||||
row_grid_columns = [
|
||||
"name",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"birthdate",
|
||||
"active",
|
||||
]
|
||||
|
||||
rows_sort_defaults = "name"
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def get_farmos_url(self, animal_type):
|
||||
return self.app.get_farmos_url(f"/taxonomy/term/{animal_type.drupal_id}")
|
||||
|
||||
def get_xref_buttons(self, animal_type):
|
||||
buttons = super().get_xref_buttons(animal_type)
|
||||
|
||||
if animal_type.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_animal_types.view", uuid=animal_type.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
def get_row_grid_data(self, animal_type):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
return session.query(model.Animal).filter(
|
||||
model.Animal.animal_type == animal_type
|
||||
)
|
||||
|
||||
def configure_row_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_row_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def get_row_action_url_view(self, animal, i):
|
||||
return self.request.route_url("animals.view", uuid=animal.uuid)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
AnimalTypeView = kwargs.get("AnimalTypeView", base["AnimalTypeView"])
|
||||
AnimalTypeView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
@ -23,30 +23,34 @@
|
|||
Master view for Animals
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.animals import Animal
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDictEnum
|
||||
from wuttaweb.util import get_form_data
|
||||
|
||||
from wuttafarm.db.model import AnimalType, AnimalAsset
|
||||
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
||||
from wuttafarm.web.forms.schema import AnimalTypeRef
|
||||
from wuttafarm.web.forms.widgets import ImageWidget
|
||||
from wuttafarm.web.util import get_farmos_client_for_user
|
||||
|
||||
|
||||
class AnimalView(WuttaFarmMasterView):
|
||||
class AnimalTypeView(AssetTypeMasterView):
|
||||
"""
|
||||
Master view for Animals
|
||||
Master view for Animal Types
|
||||
"""
|
||||
|
||||
model_class = Animal
|
||||
route_prefix = "animals"
|
||||
url_prefix = "/animals"
|
||||
model_class = AnimalType
|
||||
route_prefix = "animal_types"
|
||||
url_prefix = "/animal-types"
|
||||
|
||||
farmos_refurl_path = "/assets/animal"
|
||||
farmos_entity_type = "taxonomy_term"
|
||||
farmos_bundle = "animal_type"
|
||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/animal_type/overview"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"animal_type",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"birthdate",
|
||||
"active",
|
||||
"description",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
|
@ -57,60 +61,45 @@ class AnimalView(WuttaFarmMasterView):
|
|||
|
||||
form_fields = [
|
||||
"name",
|
||||
"animal_type",
|
||||
"birthdate",
|
||||
"description",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
has_rows = True
|
||||
row_model_class = AnimalAsset
|
||||
rows_viewable = True
|
||||
|
||||
row_grid_columns = [
|
||||
"asset_name",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"active",
|
||||
"notes",
|
||||
"farmos_uuid",
|
||||
"drupal_id",
|
||||
"image_url",
|
||||
"image",
|
||||
"birthdate",
|
||||
"archived",
|
||||
]
|
||||
|
||||
rows_sort_defaults = "asset_name"
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
# animal_type
|
||||
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, label="Animal Type Name")
|
||||
def get_farmos_url(self, animal_type):
|
||||
return self.app.get_farmos_url(f"/taxonomy/term/{animal_type.drupal_id}")
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
animal = form.model_instance
|
||||
def get_xref_buttons(self, animal_type):
|
||||
buttons = super().get_xref_buttons(animal_type)
|
||||
|
||||
# animal_type
|
||||
f.set_node("animal_type", AnimalTypeRef(self.request))
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
# image
|
||||
if animal.image_url:
|
||||
f.set_widget("image", ImageWidget("animal image"))
|
||||
f.set_default("image", animal.image_url)
|
||||
|
||||
def get_farmos_url(self, animal):
|
||||
return self.app.get_farmos_url(f"/asset/{animal.drupal_id}")
|
||||
|
||||
def get_xref_buttons(self, animal):
|
||||
buttons = super().get_xref_buttons(animal)
|
||||
|
||||
if animal.farmos_uuid:
|
||||
if animal_type.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_animals.view", uuid=animal.farmos_uuid
|
||||
"farmos_animal_types.view", uuid=animal_type.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
|
|
@ -118,12 +107,205 @@ class AnimalView(WuttaFarmMasterView):
|
|||
|
||||
return buttons
|
||||
|
||||
def delete(self):
|
||||
animal_type = self.get_instance()
|
||||
|
||||
if animal_type.animal_assets:
|
||||
self.request.session.flash(
|
||||
"Cannot delete animal type which is still referenced by animal assets.",
|
||||
"warning",
|
||||
)
|
||||
url = self.get_action_url("view", animal_type)
|
||||
return self.redirect(self.request.get_referrer(default=url))
|
||||
|
||||
return super().delete()
|
||||
|
||||
def get_row_grid_data(self, animal_type):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
return (
|
||||
session.query(model.AnimalAsset)
|
||||
.join(model.Asset)
|
||||
.filter(model.AnimalAsset.animal_type == animal_type)
|
||||
)
|
||||
|
||||
def configure_row_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_row_grid(g)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
# asset_name
|
||||
g.set_link("asset_name")
|
||||
g.set_sorter("asset_name", model.Asset.asset_name)
|
||||
g.set_filter("asset_name", model.Asset.asset_name)
|
||||
|
||||
# sex
|
||||
g.set_enum("sex", enum.ANIMAL_SEX)
|
||||
g.filters["sex"].verbs = ["equal", "not_equal"]
|
||||
|
||||
# archived
|
||||
g.set_renderer("archived", "boolean")
|
||||
g.set_sorter("archived", model.Asset.archived)
|
||||
g.set_filter("archived", model.Asset.archived)
|
||||
|
||||
def get_row_action_url_view(self, animal, i):
|
||||
return self.request.route_url("animal_assets.view", uuid=animal.uuid)
|
||||
|
||||
def ajax_create(self):
|
||||
"""
|
||||
AJAX view to create a new animal type.
|
||||
"""
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
data = get_form_data(self.request)
|
||||
|
||||
name = data.get("name")
|
||||
if not name:
|
||||
return {"error": "Name is required"}
|
||||
|
||||
animal_type = model.AnimalType(name=name)
|
||||
session.add(animal_type)
|
||||
session.flush()
|
||||
|
||||
if self.app.is_farmos_mirror():
|
||||
client = get_farmos_client_for_user(self.request)
|
||||
self.app.auto_sync_to_farmos(animal_type, client=client)
|
||||
|
||||
return {
|
||||
"uuid": animal_type.uuid.hex,
|
||||
"name": animal_type.name,
|
||||
"farmos_uuid": animal_type.farmos_uuid.hex,
|
||||
"drupal_id": animal_type.drupal_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
""" """
|
||||
cls._defaults(config)
|
||||
cls._animal_type_defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _animal_type_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
|
||||
# ajax_create
|
||||
config.add_route(f"{route_prefix}.ajax_create", f"{url_prefix}/ajax/new")
|
||||
config.add_view(
|
||||
cls,
|
||||
attr="ajax_create",
|
||||
route_name=f"{route_prefix}.ajax_create",
|
||||
permission=f"{permission_prefix}.create",
|
||||
renderer="json",
|
||||
)
|
||||
|
||||
|
||||
class AnimalAssetView(AssetMasterView):
|
||||
"""
|
||||
Master view for Animal Assets
|
||||
"""
|
||||
|
||||
model_class = AnimalAsset
|
||||
route_prefix = "animal_assets"
|
||||
url_prefix = "/assets/animal"
|
||||
|
||||
farmos_refurl_path = "/assets/animal"
|
||||
farmos_bundle = "animal"
|
||||
|
||||
labels = {
|
||||
"animal_type": "Species / Breed",
|
||||
"is_sterile": "Sterile",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"thumbnail",
|
||||
"drupal_id",
|
||||
"asset_name",
|
||||
"produces_eggs",
|
||||
"animal_type",
|
||||
"birthdate",
|
||||
"is_sterile",
|
||||
"sex",
|
||||
"groups",
|
||||
"owners",
|
||||
"locations",
|
||||
"archived",
|
||||
]
|
||||
|
||||
form_fields = [
|
||||
"asset_name",
|
||||
"animal_type",
|
||||
"birthdate",
|
||||
"produces_eggs",
|
||||
"sex",
|
||||
"is_sterile",
|
||||
"notes",
|
||||
"asset_type",
|
||||
"owners",
|
||||
"locations",
|
||||
"groups",
|
||||
"archived",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
"thumbnail_url",
|
||||
"image_url",
|
||||
"thumbnail",
|
||||
"image",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
# animal_type
|
||||
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")
|
||||
|
||||
# sex
|
||||
g.set_enum("sex", enum.ANIMAL_SEX)
|
||||
g.filters["sex"].verbs = ["equal", "not_equal"]
|
||||
|
||||
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)
|
||||
enum = self.app.enum
|
||||
animal = f.model_instance
|
||||
|
||||
# animal_type
|
||||
f.set_node("animal_type", AnimalTypeRef(self.request))
|
||||
|
||||
# sex
|
||||
if not (self.creating or self.editing) and animal.sex is None:
|
||||
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)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
AnimalView = kwargs.get("AnimalView", base["AnimalView"])
|
||||
AnimalView.defaults(config)
|
||||
AnimalTypeView = kwargs.get("AnimalTypeView", base["AnimalTypeView"])
|
||||
AnimalTypeView.defaults(config)
|
||||
|
||||
AnimalAssetView = kwargs.get("AnimalAssetView", base["AnimalAssetView"])
|
||||
AnimalAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
Master view for Asset Types
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.assets import AssetType
|
||||
from wuttafarm.db.model import AssetType
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
|
||||
|
||||
|
|
@ -38,6 +38,7 @@ class AssetTypeView(WuttaFarmMasterView):
|
|||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"drupal_id",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
|
@ -50,8 +51,8 @@ class AssetTypeView(WuttaFarmMasterView):
|
|||
form_fields = [
|
||||
"name",
|
||||
"description",
|
||||
"farmos_uuid",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
|
|
@ -78,6 +79,19 @@ class AssetTypeView(WuttaFarmMasterView):
|
|||
|
||||
return buttons
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
""" """
|
||||
wutta_config = config.registry.settings.get("wutta_config")
|
||||
app = wutta_config.get_app()
|
||||
|
||||
if app.is_farmos_mirror():
|
||||
cls.creatable = False
|
||||
cls.editable = False
|
||||
cls.deletable = False
|
||||
|
||||
cls._defaults(config)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
|||
433
src/wuttafarm/web/views/assets.py
Normal file
433
src/wuttafarm/web/views/assets.py
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
# -*- 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 Assets
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDictEnum
|
||||
from wuttaweb.db import Session
|
||||
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from wuttafarm.db.model import Asset, Log
|
||||
from wuttafarm.web.forms.schema import AssetParentRefs, OwnerRefs, AssetRefs
|
||||
from wuttafarm.web.forms.widgets import ImageWidget
|
||||
from wuttafarm.util import get_log_type_enum
|
||||
from wuttafarm.web.util import get_farmos_client_for_user
|
||||
|
||||
|
||||
def get_asset_type_enum(config):
|
||||
app = config.get_app()
|
||||
model = app.model
|
||||
session = Session()
|
||||
asset_types = OrderedDict()
|
||||
query = session.query(model.AssetType).order_by(model.AssetType.name)
|
||||
for asset_type in query:
|
||||
asset_types[asset_type.drupal_id] = asset_type.name
|
||||
return asset_types
|
||||
|
||||
|
||||
class AssetTypeMasterView(WuttaFarmMasterView):
|
||||
"""
|
||||
Base class for "Asset Type" master views.
|
||||
|
||||
A bit of a misnmer perhaps, this is *not* for the actual AssetType
|
||||
model, but rather the "secondary" types, e.g. AnimalType,
|
||||
LandType etc.
|
||||
"""
|
||||
|
||||
|
||||
class AssetMasterView(WuttaFarmMasterView):
|
||||
"""
|
||||
Base class for Asset master views
|
||||
"""
|
||||
|
||||
farmos_entity_type = "asset"
|
||||
|
||||
labels = {
|
||||
"groups": "Group Membership",
|
||||
}
|
||||
|
||||
sort_defaults = "asset_name"
|
||||
|
||||
filter_defaults = {
|
||||
"asset_name": {"active": True, "verb": "contains"},
|
||||
"archived": {"active": True, "verb": "is_false"},
|
||||
}
|
||||
|
||||
has_rows = True
|
||||
row_model_class = Log
|
||||
rows_viewable = True
|
||||
|
||||
row_labels = {
|
||||
"message": "Log Name",
|
||||
}
|
||||
|
||||
row_grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"message",
|
||||
"log_type",
|
||||
"assets",
|
||||
"location",
|
||||
"quantity",
|
||||
"is_group_assignment",
|
||||
]
|
||||
|
||||
rows_sort_defaults = ("timestamp", "desc")
|
||||
|
||||
def get_fallback_templates(self, template):
|
||||
templates = super().get_fallback_templates(template)
|
||||
|
||||
if self.viewing:
|
||||
templates.insert(0, "/assets/master/view.mako")
|
||||
|
||||
return templates
|
||||
|
||||
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.Asset:
|
||||
query = query.join(model.Asset)
|
||||
return query
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# 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", model.Asset.drupal_id)
|
||||
g.set_filter("drupal_id", model.Asset.drupal_id)
|
||||
|
||||
# asset_name
|
||||
g.set_link("asset_name")
|
||||
g.set_sorter("asset_name", model.Asset.asset_name)
|
||||
g.set_filter("asset_name", model.Asset.asset_name)
|
||||
|
||||
# parents
|
||||
g.set_renderer("parents", self.render_parents_for_grid)
|
||||
|
||||
# groups
|
||||
g.set_renderer("groups", self.render_groups_for_grid)
|
||||
|
||||
# owners
|
||||
g.set_label("owners", "Owner")
|
||||
g.set_renderer("owners", self.render_owners_for_grid)
|
||||
|
||||
# locations
|
||||
g.set_label("locations", "Location")
|
||||
g.set_renderer("locations", self.render_locations_for_grid)
|
||||
|
||||
# archived
|
||||
g.set_renderer("archived", "boolean")
|
||||
g.set_sorter("archived", model.Asset.archived)
|
||||
g.set_filter("archived", model.Asset.archived)
|
||||
|
||||
def render_parents_for_grid(self, asset, field, value):
|
||||
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for parent in asset.parents:
|
||||
url = self.request.route_url(
|
||||
f"{parent.asset_type}_assets.view", uuid=parent.uuid
|
||||
)
|
||||
links.append(tags.link_to(str(parent), url))
|
||||
return ", ".join(links)
|
||||
|
||||
parents = [str(p.parent) for p in asset.parents]
|
||||
return ", ".join(parents)
|
||||
|
||||
def render_owners_for_grid(self, asset, field, value):
|
||||
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for user in asset.owners:
|
||||
url = self.request.route_url("users.view", uuid=user.uuid)
|
||||
links.append(tags.link_to(user.username, url))
|
||||
return ", ".join(links)
|
||||
|
||||
return ", ".join([user.username for user in asset.owners])
|
||||
|
||||
def render_groups_for_grid(self, asset, field, value):
|
||||
asset_handler = self.app.get_asset_handler()
|
||||
groups = asset_handler.get_groups(asset)
|
||||
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for group in groups:
|
||||
url = self.request.route_url(
|
||||
f"{group.asset_type}_assets.view", uuid=group.uuid
|
||||
)
|
||||
links.append(tags.link_to(str(group), url))
|
||||
return ", ".join(links)
|
||||
|
||||
return ", ".join([str(group) for group in groups])
|
||||
|
||||
def render_locations_for_grid(self, asset, field, value):
|
||||
asset_handler = self.app.get_asset_handler()
|
||||
locations = asset_handler.get_locations(asset)
|
||||
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for loc in locations:
|
||||
url = self.request.route_url(
|
||||
f"{loc.asset_type}_assets.view", uuid=loc.uuid
|
||||
)
|
||||
links.append(tags.link_to(str(loc), url))
|
||||
return ", ".join(links)
|
||||
|
||||
return ", ".join([str(loc) for loc in locations])
|
||||
|
||||
def grid_row_class(self, asset, data, i):
|
||||
""" """
|
||||
if asset.archived:
|
||||
return "has-background-warning"
|
||||
return None
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
asset_handler = self.app.get_asset_handler()
|
||||
asset = form.model_instance
|
||||
|
||||
# asset_type
|
||||
if self.creating:
|
||||
f.remove("asset_type")
|
||||
else:
|
||||
f.set_node(
|
||||
"asset_type",
|
||||
WuttaDictEnum(self.request, get_asset_type_enum(self.config)),
|
||||
)
|
||||
f.set_readonly("asset_type")
|
||||
|
||||
# owners
|
||||
if self.creating or self.editing:
|
||||
f.remove("owners") # TODO: need to support this
|
||||
else:
|
||||
f.set_node("owners", OwnerRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("owners", asset.owners)
|
||||
|
||||
# locations
|
||||
if self.creating or self.editing:
|
||||
# nb. this is a calculated field
|
||||
f.remove("locations")
|
||||
else:
|
||||
f.set_label("locations", "Current Location")
|
||||
f.set_node("locations", AssetRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("locations", asset_handler.get_locations(asset))
|
||||
|
||||
# groups
|
||||
if self.creating or self.editing:
|
||||
# nb. this is a calculated field
|
||||
f.remove("groups")
|
||||
else:
|
||||
f.set_node("groups", AssetRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("groups", asset_handler.get_groups(asset))
|
||||
|
||||
# parents
|
||||
if self.creating or self.editing:
|
||||
f.remove("parents") # TODO: add support for this
|
||||
else:
|
||||
f.set_node("parents", AssetParentRefs(self.request))
|
||||
f.set_default("parents", [p.uuid for p in asset.parents])
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
# 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.thumbnail_url:
|
||||
f.set_widget("thumbnail", ImageWidget("animal thumbnail"))
|
||||
f.set_default("thumbnail", asset.thumbnail_url)
|
||||
|
||||
# image
|
||||
if self.creating or self.editing:
|
||||
f.remove("image")
|
||||
elif asset.image_url:
|
||||
f.set_widget("image", ImageWidget("animal image"))
|
||||
f.set_default("image", asset.image_url)
|
||||
|
||||
def objectify(self, form):
|
||||
asset = super().objectify(form)
|
||||
|
||||
if self.creating:
|
||||
asset.asset_type = self.get_asset_type()
|
||||
|
||||
return asset
|
||||
|
||||
def get_asset_type(self):
|
||||
model_class = self.get_model_class()
|
||||
return model_class.__wutta_hint__["farmos_asset_type"]
|
||||
|
||||
def get_farmos_url(self, asset):
|
||||
return self.app.get_farmos_url(f"/asset/{asset.drupal_id}")
|
||||
|
||||
def get_xref_buttons(self, asset):
|
||||
buttons = super().get_xref_buttons(asset)
|
||||
|
||||
if asset.farmos_uuid:
|
||||
asset_type = self.get_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
|
||||
|
||||
def get_version_joins(self):
|
||||
"""
|
||||
We override this to declare the relationship between the
|
||||
view's data model (which is some type of asset table) and the
|
||||
canonical ``Asset`` model, so the revision history views
|
||||
include transactions which reference either version table.
|
||||
|
||||
See also parent method,
|
||||
:meth:`~wuttaweb:wuttaweb.views.master.MasterView.get_version_joins()`
|
||||
"""
|
||||
model = self.app.model
|
||||
return super().get_version_joins() + [
|
||||
model.Asset,
|
||||
]
|
||||
|
||||
def get_row_grid_data(self, asset):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
return (
|
||||
session.query(model.Log)
|
||||
.outerjoin(model.LogAsset)
|
||||
.filter(model.LogAsset.asset_uuid == asset.uuid)
|
||||
)
|
||||
|
||||
def configure_row_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_row_grid(g)
|
||||
enum = self.app.enum
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
# status
|
||||
g.set_enum("status", enum.LOG_STATUS)
|
||||
|
||||
# drupal_id
|
||||
g.set_label("drupal_id", "ID", column_only=True)
|
||||
|
||||
# message
|
||||
g.set_link("message")
|
||||
g.set_sorter("message", model.Log.message)
|
||||
g.set_filter("message", model.Log.message)
|
||||
|
||||
# timestamp
|
||||
g.set_sorter("timestamp", model.Log.timestamp)
|
||||
g.set_filter("timestamp", model.Log.timestamp)
|
||||
|
||||
# log_type
|
||||
g.set_sorter("log_type", model.Log.log_type)
|
||||
g.set_filter("log_type", model.Log.log_type)
|
||||
g.set_enum("log_type", get_log_type_enum(self.config, session=session))
|
||||
|
||||
def get_row_action_url_view(self, log, i):
|
||||
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
||||
|
||||
|
||||
class AllAssetView(AssetMasterView):
|
||||
"""
|
||||
Master view for Assets
|
||||
"""
|
||||
|
||||
model_class = Asset
|
||||
route_prefix = "assets"
|
||||
url_prefix = "/assets"
|
||||
|
||||
farmos_refurl_path = "/assets"
|
||||
|
||||
viewable = False
|
||||
creatable = False
|
||||
editable = False
|
||||
deletable = False
|
||||
model_is_versioned = False
|
||||
|
||||
grid_columns = [
|
||||
"thumbnail",
|
||||
"drupal_id",
|
||||
"asset_name",
|
||||
"groups",
|
||||
"asset_type",
|
||||
"parents",
|
||||
"owners",
|
||||
"locations",
|
||||
"archived",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# asset_type
|
||||
g.set_enum("asset_type", get_asset_type_enum(self.config))
|
||||
|
||||
# view action links to final asset record
|
||||
def asset_url(asset, i):
|
||||
return self.request.route_url(
|
||||
f"{asset.asset_type}_assets.view", uuid=asset.uuid
|
||||
)
|
||||
|
||||
g.add_action("view", icon="eye", url=asset_url)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
AllAssetView = kwargs.get("AllAssetView", base["AllAssetView"])
|
||||
AllAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
@ -55,9 +55,10 @@ class AuthView(base.AuthView):
|
|||
return None
|
||||
|
||||
def get_farmos_oauth2_session(self):
|
||||
farmos = self.app.get_farmos_handler()
|
||||
return OAuth2Session(
|
||||
client_id="farm",
|
||||
scope="farm_manager",
|
||||
client_id=farmos.get_oauth2_client_id(),
|
||||
scope=farmos.get_oauth2_scope(),
|
||||
redirect_uri=self.request.route_url("farmos_oauth_callback"),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,26 +51,28 @@ 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",
|
||||
"animal_types.view",
|
||||
"animal_types.versions",
|
||||
"animals.list",
|
||||
"animals.view",
|
||||
"animals.versions",
|
||||
"animal_assets.create",
|
||||
"animal_assets.edit",
|
||||
"animal_assets.list",
|
||||
"animal_assets.view",
|
||||
"animal_assets.versions",
|
||||
"assets.list",
|
||||
"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",
|
||||
|
|
@ -79,15 +81,35 @@ 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_plant_assets.list",
|
||||
"farmos_plant_assets.view",
|
||||
"farmos_plant_types.list",
|
||||
"farmos_plant_types.view",
|
||||
"farmos_quantities_standard.list",
|
||||
"farmos_quantities_standard.view",
|
||||
"farmos_quantity_types.list",
|
||||
"farmos_quantity_types.view",
|
||||
"farmos_structure_assets.list",
|
||||
"farmos_structure_assets.view",
|
||||
"farmos_structure_types.list",
|
||||
"farmos_structure_types.view",
|
||||
"farmos_structures.list",
|
||||
"farmos_structures.view",
|
||||
"farmos_units.list",
|
||||
"farmos_units.view",
|
||||
"farmos_users.list",
|
||||
"farmos_users.view",
|
||||
"groups.list",
|
||||
"groups.view",
|
||||
"groups.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",
|
||||
"land_assets.view",
|
||||
"land_assets.versions",
|
||||
|
|
@ -97,12 +119,32 @@ 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",
|
||||
"quick.eggs",
|
||||
"structure_types.list",
|
||||
"structure_types.view",
|
||||
"structure_types.versions",
|
||||
"structures.list",
|
||||
"structures.view",
|
||||
"structures.versions",
|
||||
"structure_assets.create",
|
||||
"structure_assets.edit",
|
||||
"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")
|
||||
|
|
@ -36,5 +38,9 @@ def includeme(config):
|
|||
config.include("wuttafarm.web.views.farmos.animal_types")
|
||||
config.include("wuttafarm.web.views.farmos.animals")
|
||||
config.include("wuttafarm.web.views.farmos.groups")
|
||||
config.include("wuttafarm.web.views.farmos.plants")
|
||||
config.include("wuttafarm.web.views.farmos.log_types")
|
||||
config.include("wuttafarm.web.views.farmos.logs_activity")
|
||||
config.include("wuttafarm.web.views.farmos.logs_harvest")
|
||||
config.include("wuttafarm.web.views.farmos.logs_medical")
|
||||
config.include("wuttafarm.web.views.farmos.logs_observation")
|
||||
|
|
|
|||
|
|
@ -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, FarmOSAssetRefs
|
||||
|
||||
|
||||
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",
|
||||
"owners",
|
||||
"locations",
|
||||
"groups",
|
||||
"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["groups"]:
|
||||
if self.farmos_style_grid_links:
|
||||
url = self.request.route_url(
|
||||
"farmos_group_assets.view", uuid=group["uuid"]
|
||||
)
|
||||
groups.append(tags.link_to(group["name"], url))
|
||||
else:
|
||||
groups.append(group["name"])
|
||||
return ", ".join(groups)
|
||||
|
||||
def get_instance(self):
|
||||
|
||||
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,85 +191,141 @@ 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"],
|
||||
"asset_type": "group",
|
||||
}
|
||||
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"],
|
||||
"groups": 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
|
||||
else:
|
||||
f.set_node("groups", FarmOSAssetRefs(self.request))
|
||||
|
||||
# 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.Animal)
|
||||
.filter(model.Animal.farmos_uuid == animal["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url("animals.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
|
||||
|
||||
|
|
|
|||
319
src/wuttafarm/web/views/farmos/assets.py
Normal file
319
src/wuttafarm/web/views/farmos/assets.py
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
# -*- 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",
|
||||
"locations": "Location",
|
||||
"groups": "Group Membership",
|
||||
"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_label("owners", "Owner")
|
||||
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_label("locations", "Current Location")
|
||||
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"
|
||||
|
|
@ -115,6 +115,9 @@ class GroupView(FarmOSMasterView):
|
|||
else:
|
||||
archived = group["attributes"]["status"] == "archived"
|
||||
|
||||
if notes := group["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
|
||||
return {
|
||||
"uuid": group["id"],
|
||||
"drupal_id": group["attributes"]["drupal_internal__id"],
|
||||
|
|
@ -124,7 +127,7 @@ class GroupView(FarmOSMasterView):
|
|||
"is_fixed": group["attributes"]["is_fixed"],
|
||||
"is_location": group["attributes"]["is_location"],
|
||||
"archived": archived,
|
||||
"notes": group["attributes"]["notes"]["value"],
|
||||
"notes": notes or colander.null,
|
||||
}
|
||||
|
||||
def configure_form(self, form):
|
||||
|
|
@ -166,15 +169,15 @@ class GroupView(FarmOSMasterView):
|
|||
]
|
||||
|
||||
if wf_group := (
|
||||
session.query(model.Group)
|
||||
.filter(model.Group.farmos_uuid == group["uuid"])
|
||||
session.query(model.GroupAsset)
|
||||
.filter(model.GroupAsset.farmos_uuid == group["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url("groups.view", uuid=wf_group.uuid),
|
||||
url=self.request.route_url("group_assets.view", uuid=wf_group.uuid),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
|
|
|||
289
src/wuttafarm/web/views/farmos/logs.py
Normal file
289
src/wuttafarm/web/views/farmos/logs.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
# -*- 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 Harvest Logs
|
||||
"""
|
||||
|
||||
import colander
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDateTime, WuttaDictEnum
|
||||
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,
|
||||
Notes,
|
||||
)
|
||||
from wuttafarm.web.util import render_quantity_objects
|
||||
|
||||
|
||||
class LogMasterView(FarmOSMasterView):
|
||||
"""
|
||||
Base class for farmOS Log master views
|
||||
"""
|
||||
|
||||
farmos_log_type = None
|
||||
filterable = True
|
||||
sort_on_backend = True
|
||||
|
||||
labels = {
|
||||
"name": "Log Name",
|
||||
"log_type_name": "Log Type",
|
||||
"locations": "Location",
|
||||
"quantities": "Quantity",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"locations",
|
||||
"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",
|
||||
"assets",
|
||||
"groups",
|
||||
"locations",
|
||||
"quantities",
|
||||
"notes",
|
||||
"status",
|
||||
"log_type_name",
|
||||
"owners",
|
||||
"is_movement",
|
||||
"is_group_assignment",
|
||||
"quick",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
def get_farmos_api_includes(self):
|
||||
return {"log_type", "quantity", "asset", "group", "location", "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_sorter("name", SimpleSorter("name"))
|
||||
g.set_filter("name", StringFilter)
|
||||
|
||||
# assets
|
||||
g.set_renderer("assets", self.render_assets_for_grid)
|
||||
|
||||
# groups
|
||||
g.set_renderer("groups", self.render_assets_for_grid)
|
||||
|
||||
# locations
|
||||
g.set_renderer("locations", 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):
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
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):
|
||||
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 = 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, 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))
|
||||
|
||||
# groups
|
||||
f.set_node("groups", FarmOSAssetRefs(self.request))
|
||||
|
||||
# locations
|
||||
f.set_node("locations", FarmOSAssetRefs(self.request))
|
||||
|
||||
# quantities
|
||||
f.set_node("quantities", FarmOSQuantityRefs(self.request))
|
||||
|
||||
# is_movement
|
||||
f.set_node("is_movement", colander.Boolean())
|
||||
|
||||
# is_group_assignment
|
||||
f.set_node("is_group_assignment", colander.Boolean())
|
||||
|
||||
# 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
|
||||
session = self.Session()
|
||||
|
||||
buttons = [
|
||||
self.make_button(
|
||||
"View in farmOS",
|
||||
primary=True,
|
||||
url=self.app.get_farmos_url(f"/log/{log['drupal_id']}"),
|
||||
target="_blank",
|
||||
icon_left="external-link-alt",
|
||||
),
|
||||
]
|
||||
|
||||
if wf_log := (
|
||||
session.query(model.Log)
|
||||
.filter(model.Log.farmos_uuid == log["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
f"logs_{self.farmos_log_type}.view", uuid=wf_log.uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
|
@ -20,20 +20,13 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
View for farmOS activity logs
|
||||
View for farmOS Activity Logs
|
||||
"""
|
||||
|
||||
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.views.farmos.logs import LogMasterView
|
||||
|
||||
|
||||
class ActivityLogView(FarmOSMasterView):
|
||||
class ActivityLogView(LogMasterView):
|
||||
"""
|
||||
View for farmOS activity logs
|
||||
"""
|
||||
|
|
@ -45,105 +38,9 @@ class ActivityLogView(FarmOSMasterView):
|
|||
route_prefix = "farmos_logs_activity"
|
||||
url_prefix = "/farmOS/logs/activity"
|
||||
|
||||
farmos_log_type = "activity"
|
||||
farmos_refurl_path = "/logs/activity"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"timestamp",
|
||||
"status",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"timestamp",
|
||||
"status",
|
||||
"notes",
|
||||
]
|
||||
|
||||
def get_grid_data(self, columns=None, session=None):
|
||||
logs = self.farmos_client.log.get("activity")
|
||||
return [self.normalize_log(t) for t in logs["data"]]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
g.set_searchable("name")
|
||||
|
||||
# timestamp
|
||||
g.set_renderer("timestamp", "datetime")
|
||||
|
||||
def get_instance(self):
|
||||
log = self.farmos_client.log.get_id("activity", self.request.matchdict["uuid"])
|
||||
self.raw_json = log
|
||||
return self.normalize_log(log["data"])
|
||||
|
||||
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 configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
||||
# timestamp
|
||||
f.set_node("timestamp", WuttaDateTime())
|
||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
def get_xref_buttons(self, log):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
buttons = [
|
||||
self.make_button(
|
||||
"View in farmOS",
|
||||
primary=True,
|
||||
url=self.app.get_farmos_url(f"/log/{log['drupal_id']}"),
|
||||
target="_blank",
|
||||
icon_left="external-link-alt",
|
||||
),
|
||||
]
|
||||
|
||||
if wf_log := (
|
||||
session.query(model.ActivityLog)
|
||||
.filter(model.ActivityLog.farmos_uuid == log["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url("activity_logs.view", uuid=wf_log.uuid),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
|||
63
src/wuttafarm/web/views/farmos/logs_harvest.py
Normal file
63
src/wuttafarm/web/views/farmos/logs_harvest.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# -*- 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 Harvest Logs
|
||||
"""
|
||||
|
||||
from wuttafarm.web.views.farmos.logs import LogMasterView
|
||||
|
||||
|
||||
class HarvestLogView(LogMasterView):
|
||||
"""
|
||||
View for farmOS harvest logs
|
||||
"""
|
||||
|
||||
model_name = "farmos_harvest_log"
|
||||
model_title = "farmOS Harvest Log"
|
||||
model_title_plural = "farmOS Harvest Logs"
|
||||
|
||||
route_prefix = "farmos_logs_harvest"
|
||||
url_prefix = "/farmOS/logs/harvest"
|
||||
|
||||
farmos_log_type = "harvest"
|
||||
farmos_refurl_path = "/logs/harvest"
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"quantities",
|
||||
"owners",
|
||||
]
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
HarvestLogView = kwargs.get("HarvestLogView", base["HarvestLogView"])
|
||||
HarvestLogView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
83
src/wuttafarm/web/views/farmos/logs_medical.py
Normal file
83
src/wuttafarm/web/views/farmos/logs_medical.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# -*- 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 Medical Logs
|
||||
"""
|
||||
|
||||
from wuttafarm.web.views.farmos.logs import LogMasterView
|
||||
from wuttafarm.web.grids import SimpleSorter, StringFilter
|
||||
|
||||
|
||||
class MedicalLogView(LogMasterView):
|
||||
"""
|
||||
View for farmOS medical logs
|
||||
"""
|
||||
|
||||
model_name = "farmos_medical_log"
|
||||
model_title = "farmOS Medical Log"
|
||||
model_title_plural = "farmOS Medical Logs"
|
||||
|
||||
route_prefix = "farmos_logs_medical"
|
||||
url_prefix = "/farmOS/logs/medical"
|
||||
|
||||
farmos_log_type = "medical"
|
||||
farmos_refurl_path = "/logs/medical"
|
||||
|
||||
labels = {
|
||||
"vet": "Veterinarian",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"vet",
|
||||
"owners",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# vet
|
||||
g.set_sorter("vet", SimpleSorter("vet"))
|
||||
g.set_filter("vet", StringFilter)
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
||||
# vet
|
||||
f.fields.insert_after("timestamp", "vet")
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
MedicalLogView = kwargs.get("MedicalLogView", base["MedicalLogView"])
|
||||
MedicalLogView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
65
src/wuttafarm/web/views/farmos/logs_observation.py
Normal file
65
src/wuttafarm/web/views/farmos/logs_observation.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# -*- 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 Observation Logs
|
||||
"""
|
||||
|
||||
from wuttafarm.web.views.farmos.logs import LogMasterView
|
||||
|
||||
|
||||
class ObservationLogView(LogMasterView):
|
||||
"""
|
||||
View for farmOS observation logs
|
||||
"""
|
||||
|
||||
model_name = "farmos_observation_log"
|
||||
model_title = "farmOS Observation Log"
|
||||
model_title_plural = "farmOS Observation Logs"
|
||||
|
||||
route_prefix = "farmos_logs_observation"
|
||||
url_prefix = "/farmOS/logs/observation"
|
||||
|
||||
farmos_log_type = "observation"
|
||||
farmos_refurl_path = "/logs/observation"
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"locations",
|
||||
"groups",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
ObservationLogView = kwargs.get("ObservationLogView", base["ObservationLogView"])
|
||||
ObservationLogView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
@ -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 get_farmos_client_for_user, 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",
|
||||
|
|
@ -57,25 +70,11 @@ class FarmOSMasterView(MasterView):
|
|||
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.farmos_client = self.get_farmos_client()
|
||||
self.farmos_client = get_farmos_client_for_user(self.request)
|
||||
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
||||
self.normal = self.app.get_normalizer(self.farmos_client)
|
||||
self.raw_json = None
|
||||
|
||||
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)
|
||||
self.farmos_style_grid_links = use_farmos_style_grid_links(self.config)
|
||||
|
||||
def get_fallback_templates(self, template):
|
||||
""" """
|
||||
|
|
@ -86,6 +85,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 +109,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",
|
||||
)
|
||||
]
|
||||
|
|
|
|||
287
src/wuttafarm/web/views/farmos/plants.py
Normal file
287
src/wuttafarm/web/views/farmos/plants.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# -*- 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 Farm Plants
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
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(TaxonomyMasterView):
|
||||
"""
|
||||
Master view for Plant Types in farmOS.
|
||||
"""
|
||||
|
||||
model_name = "farmos_plant_type"
|
||||
model_title = "farmOS Plant Type"
|
||||
model_title_plural = "farmOS Plant Types"
|
||||
|
||||
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"
|
||||
|
||||
def get_xref_buttons(self, plant_type):
|
||||
buttons = super().get_xref_buttons(plant_type)
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
if wf_plant_type := (
|
||||
session.query(model.PlantType)
|
||||
.filter(model.PlantType.farmos_uuid == plant_type["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"plant_types.view", uuid=wf_plant_type.uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
class PlantAssetView(FarmOSMasterView):
|
||||
"""
|
||||
Master view for farmOS Plant Assets
|
||||
"""
|
||||
|
||||
model_name = "farmos_plant_assets"
|
||||
model_title = "farmOS Plant Asset"
|
||||
model_title_plural = "farmOS Plant Assets"
|
||||
|
||||
route_prefix = "farmos_plant_assets"
|
||||
url_prefix = "/farmOS/assets/plant"
|
||||
|
||||
farmos_refurl_path = "/assets/plant"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"archived",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"plant_types",
|
||||
"archived",
|
||||
"owners",
|
||||
"location",
|
||||
"notes",
|
||||
"raw_image_url",
|
||||
"large_image_url",
|
||||
"thumbnail_image_url",
|
||||
"image",
|
||||
]
|
||||
|
||||
def get_grid_data(self, columns=None, session=None):
|
||||
result = self.farmos_client.asset.get("plant")
|
||||
return [self.normalize_plant(a) for a in result["data"]]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
g.set_searchable("name")
|
||||
|
||||
# archived
|
||||
g.set_renderer("archived", "boolean")
|
||||
|
||||
def get_instance(self):
|
||||
|
||||
plant = self.farmos_client.resource.get_id(
|
||||
"asset", "plant", self.request.matchdict["uuid"]
|
||||
)
|
||||
self.raw_json = plant
|
||||
|
||||
# instance data
|
||||
data = self.normalize_plant(plant["data"])
|
||||
|
||||
if relationships := plant["data"].get("relationships"):
|
||||
|
||||
# add plant types
|
||||
if plant_type := relationships.get("plant_type"):
|
||||
if plant_type["data"]:
|
||||
data["plant_types"] = []
|
||||
for plant_type in plant_type["data"]:
|
||||
plant_type = self.farmos_client.resource.get_id(
|
||||
"taxonomy_term", "plant_type", plant_type["id"]
|
||||
)
|
||||
data["plant_types"].append(
|
||||
{
|
||||
"uuid": plant_type["data"]["id"],
|
||||
"name": plant_type["data"]["attributes"]["name"],
|
||||
}
|
||||
)
|
||||
|
||||
# add location
|
||||
if location := relationships.get("location"):
|
||||
if location["data"]:
|
||||
location = self.farmos_client.resource.get_id(
|
||||
"asset", "structure", location["data"][0]["id"]
|
||||
)
|
||||
data["location"] = {
|
||||
"uuid": location["data"]["id"],
|
||||
"name": location["data"]["attributes"]["name"],
|
||||
}
|
||||
|
||||
# add owners
|
||||
if owner := relationships.get("owner"):
|
||||
data["owners"] = []
|
||||
for owner_data in owner["data"]:
|
||||
owner = self.farmos_client.resource.get_id(
|
||||
"user", "user", owner_data["id"]
|
||||
)
|
||||
data["owners"].append(
|
||||
{
|
||||
"uuid": owner["data"]["id"],
|
||||
"display_name": owner["data"]["attributes"]["display_name"],
|
||||
}
|
||||
)
|
||||
|
||||
# add image urls
|
||||
if image := relationships.get("image"):
|
||||
if image["data"]:
|
||||
image = self.farmos_client.resource.get_id(
|
||||
"file", "file", image["data"][0]["id"]
|
||||
)
|
||||
data["raw_image_url"] = self.app.get_farmos_url(
|
||||
image["data"]["attributes"]["uri"]["url"]
|
||||
)
|
||||
# nb. other styles available: medium, wide
|
||||
data["large_image_url"] = image["data"]["attributes"][
|
||||
"image_style_uri"
|
||||
]["large"]
|
||||
data["thumbnail_image_url"] = image["data"]["attributes"][
|
||||
"image_style_uri"
|
||||
]["thumbnail"]
|
||||
|
||||
return data
|
||||
|
||||
def get_instance_title(self, plant):
|
||||
return plant["name"]
|
||||
|
||||
def normalize_plant(self, plant):
|
||||
|
||||
if notes := plant["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
|
||||
if self.farmos_4x:
|
||||
archived = plant["attributes"]["archived"]
|
||||
else:
|
||||
archived = plant["attributes"]["status"] == "archived"
|
||||
|
||||
return {
|
||||
"uuid": plant["id"],
|
||||
"drupal_id": plant["attributes"]["drupal_internal__id"],
|
||||
"name": plant["attributes"]["name"],
|
||||
"location": colander.null, # TODO
|
||||
"archived": archived,
|
||||
"notes": notes or colander.null,
|
||||
}
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
plant = f.model_instance
|
||||
|
||||
# plant_types
|
||||
f.set_node("plant_types", FarmOSPlantTypes(self.request))
|
||||
|
||||
# location
|
||||
f.set_node("location", StructureType(self.request))
|
||||
|
||||
# owners
|
||||
f.set_node("owners", UsersType(self.request))
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
# archived
|
||||
f.set_node("archived", colander.Boolean())
|
||||
|
||||
# image
|
||||
if url := plant.get("large_image_url"):
|
||||
f.set_widget("image", ImageWidget("plant image"))
|
||||
f.set_default("image", url)
|
||||
|
||||
def get_xref_buttons(self, plant):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
buttons = [
|
||||
self.make_button(
|
||||
"View in farmOS",
|
||||
primary=True,
|
||||
url=self.app.get_farmos_url(f"/asset/{plant['drupal_id']}"),
|
||||
target="_blank",
|
||||
icon_left="external-link-alt",
|
||||
),
|
||||
]
|
||||
|
||||
if wf_plant := (
|
||||
session.query(model.Asset)
|
||||
.filter(model.Asset.farmos_uuid == plant["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url("plant_assets.view", uuid=wf_plant.uuid),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
|
||||
PlantTypeView.defaults(config)
|
||||
|
||||
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
|
||||
PlantAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
345
src/wuttafarm/web/views/farmos/quantities.py
Normal file
345
src/wuttafarm/web/views/farmos/quantities.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
# -*- 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
|
||||
from wuttafarm.web.grids import ResourceData
|
||||
|
||||
|
||||
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 = [
|
||||
"drupal_id",
|
||||
"as_text",
|
||||
"measure",
|
||||
"value",
|
||||
"unit",
|
||||
"label",
|
||||
]
|
||||
|
||||
sort_defaults = ("drupal_id", "desc")
|
||||
|
||||
form_fields = [
|
||||
"measure",
|
||||
"value",
|
||||
"units",
|
||||
"label",
|
||||
"created",
|
||||
"changed",
|
||||
]
|
||||
|
||||
def get_farmos_api_includes(self):
|
||||
return {"units"}
|
||||
|
||||
def get_grid_data(self, **kwargs):
|
||||
return ResourceData(
|
||||
self.config,
|
||||
self.farmos_client,
|
||||
f"quantity--{self.farmos_quantity_type}",
|
||||
include=",".join(self.get_farmos_api_includes()),
|
||||
normalizer=self.normalize_quantity,
|
||||
)
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# drupal_id
|
||||
g.set_label("drupal_id", "ID", column_only=True)
|
||||
|
||||
# as_text
|
||||
g.set_renderer("as_text", self.render_as_text_for_grid)
|
||||
|
||||
# measure
|
||||
g.set_renderer("measure", self.render_measure_for_grid)
|
||||
|
||||
# value
|
||||
g.set_renderer("value", self.render_value_for_grid)
|
||||
|
||||
# unit
|
||||
g.set_renderer("unit", self.render_unit_for_grid)
|
||||
|
||||
# changed
|
||||
g.set_renderer("changed", "datetime")
|
||||
|
||||
def render_as_text_for_grid(self, qty, field, value):
|
||||
measure = qty["measure"].capitalize()
|
||||
value = qty["value"]["decimal"]
|
||||
units = qty["unit"]["name"] if qty["unit"] else "??"
|
||||
return f"( {measure} ) {value} {units}"
|
||||
|
||||
def render_measure_for_grid(self, qty, field, value):
|
||||
return qty["measure"].capitalize()
|
||||
|
||||
def render_unit_for_grid(self, qty, field, value):
|
||||
unit = qty[field]
|
||||
if not unit:
|
||||
return ""
|
||||
return unit["name"]
|
||||
|
||||
def render_value_for_grid(self, qty, field, value):
|
||||
return qty["value"]["decimal"]
|
||||
|
||||
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, included={}):
|
||||
|
||||
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)
|
||||
|
||||
quantity_type_object = None
|
||||
quantity_type_uuid = None
|
||||
unit_object = None
|
||||
unit_uuid = None
|
||||
if relationships := quantity["relationships"]:
|
||||
|
||||
if quantity_type := relationships["quantity_type"]["data"]:
|
||||
quantity_type_uuid = quantity_type["id"]
|
||||
quantity_type_object = {
|
||||
"uuid": quantity_type_uuid,
|
||||
"type": "quantity_type--quantity_type",
|
||||
}
|
||||
|
||||
if unit := relationships["units"]["data"]:
|
||||
unit_uuid = unit["id"]
|
||||
if unit := included.get(unit_uuid):
|
||||
unit_object = {
|
||||
"uuid": unit_uuid,
|
||||
"type": "taxonomy_term--unit",
|
||||
"name": unit["attributes"]["name"],
|
||||
}
|
||||
|
||||
return {
|
||||
"uuid": quantity["id"],
|
||||
"drupal_id": quantity["attributes"]["drupal_internal__id"],
|
||||
"quantity_type": quantity_type_object,
|
||||
"quantity_type_uuid": quantity_type_uuid,
|
||||
"measure": quantity["attributes"]["measure"],
|
||||
"value": quantity["attributes"]["value"],
|
||||
"unit": unit_object,
|
||||
"unit_uuid": unit_uuid,
|
||||
"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"
|
||||
|
|
@ -211,8 +211,8 @@ class StructureView(FarmOSMasterView):
|
|||
]
|
||||
|
||||
if wf_structure := (
|
||||
session.query(model.Structure)
|
||||
.filter(model.Structure.farmos_uuid == structure["uuid"])
|
||||
session.query(model.StructureAsset)
|
||||
.filter(model.StructureAsset.farmos_uuid == structure["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
|
|
@ -220,7 +220,7 @@ class StructureView(FarmOSMasterView):
|
|||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"structures.view", uuid=wf_structure.uuid
|
||||
"structure_assets.view", uuid=wf_structure.uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,58 +20,42 @@
|
|||
#
|
||||
################################################################################
|
||||
"""
|
||||
Master view for Log Types
|
||||
View for farmOS units
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.logs import LogType
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from wuttafarm.web.views.farmos.master import TaxonomyMasterView
|
||||
|
||||
|
||||
class LogTypeView(WuttaFarmMasterView):
|
||||
class UnitView(TaxonomyMasterView):
|
||||
"""
|
||||
Master view for Log Types
|
||||
Master view for Units in farmOS.
|
||||
"""
|
||||
|
||||
model_class = LogType
|
||||
route_prefix = "log_types"
|
||||
url_prefix = "/log-types"
|
||||
model_name = "farmos_unit"
|
||||
model_title = "farmOS Unit"
|
||||
model_title_plural = "farmOS Units"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"description",
|
||||
]
|
||||
route_prefix = "farmos_units"
|
||||
url_prefix = "/farmOS/units"
|
||||
|
||||
sort_defaults = "name"
|
||||
farmos_taxonomy_type = "unit"
|
||||
farmos_refurl_path = "/admin/structure/taxonomy/manage/unit/overview"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
def get_xref_buttons(self, unit):
|
||||
buttons = super().get_xref_buttons(unit)
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
|
||||
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, log_type):
|
||||
buttons = super().get_xref_buttons(log_type)
|
||||
|
||||
if log_type.farmos_uuid:
|
||||
if wf_unit := (
|
||||
session.query(model.Unit)
|
||||
.filter(model.Unit.farmos_uuid == unit["uuid"])
|
||||
.first()
|
||||
):
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
f"View {self.app.get_title()} record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_log_types.view", uuid=log_type.farmos_uuid
|
||||
),
|
||||
url=self.request.route_url("units.view", uuid=wf_unit.uuid),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
|
@ -82,8 +66,8 @@ class LogTypeView(WuttaFarmMasterView):
|
|||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"])
|
||||
LogTypeView.defaults(config)
|
||||
UnitView = kwargs.get("UnitView", base["UnitView"])
|
||||
UnitView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
|
@ -23,78 +23,40 @@
|
|||
Master view for Groups
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.groups import Group
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from wuttafarm.web.views.assets import AssetMasterView
|
||||
from wuttafarm.db.model import GroupAsset
|
||||
|
||||
|
||||
class GroupView(WuttaFarmMasterView):
|
||||
class GroupView(AssetMasterView):
|
||||
"""
|
||||
Master view for Groups
|
||||
"""
|
||||
|
||||
model_class = Group
|
||||
route_prefix = "groups"
|
||||
url_prefix = "/groups"
|
||||
model_class = GroupAsset
|
||||
route_prefix = "group_assets"
|
||||
url_prefix = "/assets/group"
|
||||
|
||||
farmos_refurl_path = "/assets/group"
|
||||
farmos_bundle = "group"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"active",
|
||||
"thumbnail",
|
||||
"drupal_id",
|
||||
"asset_name",
|
||||
"produces_eggs",
|
||||
"archived",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"active",
|
||||
"asset_name",
|
||||
"notes",
|
||||
"farmos_uuid",
|
||||
"asset_type",
|
||||
"produces_eggs",
|
||||
"archived",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
def get_farmos_url(self, group):
|
||||
return self.app.get_farmos_url(f"/asset/{group.drupal_id}")
|
||||
|
||||
def get_xref_buttons(self, group):
|
||||
buttons = super().get_xref_buttons(group)
|
||||
|
||||
if group.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_groups.view", uuid=group.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
|||
210
src/wuttafarm/web/views/land.py
Normal file
210
src/wuttafarm/web/views/land.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# -*- 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 Land Types
|
||||
"""
|
||||
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
||||
from wuttafarm.db.model import LandType, LandAsset
|
||||
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
|
||||
from wuttafarm.web.forms.schema import LandTypeRef
|
||||
|
||||
|
||||
class LandTypeView(AssetTypeMasterView):
|
||||
"""
|
||||
Master view for Land Types
|
||||
"""
|
||||
|
||||
model_class = LandType
|
||||
route_prefix = "land_types"
|
||||
url_prefix = "/land-types"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
has_rows = True
|
||||
row_model_class = LandAsset
|
||||
rows_viewable = True
|
||||
|
||||
row_grid_columns = [
|
||||
"asset_name",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"archived",
|
||||
]
|
||||
|
||||
rows_sort_defaults = "asset_name"
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def get_xref_buttons(self, land_type):
|
||||
buttons = super().get_xref_buttons(land_type)
|
||||
|
||||
if land_type.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_land_types.view", uuid=land_type.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
def get_row_grid_data(self, land_type):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
return (
|
||||
session.query(model.LandAsset)
|
||||
.join(model.Asset)
|
||||
.filter(model.LandAsset.land_type == land_type)
|
||||
)
|
||||
|
||||
def configure_row_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_row_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# asset_name
|
||||
g.set_link("asset_name")
|
||||
g.set_sorter("asset_name", model.Asset.asset_name)
|
||||
g.set_filter("asset_name", model.Asset.asset_name)
|
||||
|
||||
# is_location
|
||||
g.set_renderer("is_location", "boolean")
|
||||
g.set_sorter("is_location", model.Asset.is_location)
|
||||
g.set_filter("is_location", model.Asset.is_location)
|
||||
|
||||
# is_fixed
|
||||
g.set_renderer("is_fixed", "boolean")
|
||||
g.set_sorter("is_fixed", model.Asset.is_fixed)
|
||||
g.set_filter("is_fixed", model.Asset.is_fixed)
|
||||
|
||||
# archived
|
||||
g.set_renderer("archived", "boolean")
|
||||
g.set_sorter("archived", model.Asset.archived)
|
||||
g.set_filter("archived", model.Asset.archived)
|
||||
|
||||
def get_row_action_url_view(self, land_asset, i):
|
||||
return self.request.route_url("land_assets.view", uuid=land_asset.uuid)
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
""" """
|
||||
wutta_config = config.registry.settings.get("wutta_config")
|
||||
app = wutta_config.get_app()
|
||||
|
||||
if app.is_farmos_mirror():
|
||||
cls.creatable = False
|
||||
cls.editable = False
|
||||
cls.deletable = False
|
||||
|
||||
cls._defaults(config)
|
||||
|
||||
|
||||
class LandAssetView(AssetMasterView):
|
||||
"""
|
||||
Master view for Land Assets
|
||||
"""
|
||||
|
||||
model_class = LandAsset
|
||||
route_prefix = "land_assets"
|
||||
url_prefix = "/assets/land"
|
||||
|
||||
farmos_bundle = "land"
|
||||
farmos_refurl_path = "/assets/land"
|
||||
|
||||
grid_columns = [
|
||||
"thumbnail",
|
||||
"drupal_id",
|
||||
"asset_name",
|
||||
"land_type",
|
||||
"parents",
|
||||
"archived",
|
||||
]
|
||||
|
||||
form_fields = [
|
||||
"asset_name",
|
||||
"parents",
|
||||
"notes",
|
||||
"asset_type",
|
||||
"land_type",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"archived",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# land_type
|
||||
g.set_joiner("land_type", lambda q: q.join(model.LandType))
|
||||
g.set_sorter("land_type", model.LandType.name)
|
||||
g.set_filter("land_type", model.LandType.name, label="Land Type Name")
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
land = f.model_instance
|
||||
|
||||
# land_type
|
||||
f.set_node("land_type", LandTypeRef(self.request))
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
LandTypeView = kwargs.get("LandTypeView", base["LandTypeView"])
|
||||
LandTypeView.defaults(config)
|
||||
|
||||
LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"])
|
||||
LandAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Master view for Land Assets
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.land import LandAsset
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from wuttafarm.web.forms.schema import LandTypeRef
|
||||
|
||||
|
||||
class LandAssetView(WuttaFarmMasterView):
|
||||
"""
|
||||
Master view for Land Assets
|
||||
"""
|
||||
|
||||
model_class = LandAsset
|
||||
route_prefix = "land_assets"
|
||||
url_prefix = "/land-assets"
|
||||
|
||||
farmos_refurl_path = "/assets/land"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"land_type",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"active",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"land_type",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"notes",
|
||||
"active",
|
||||
"farmos_uuid",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
# land_type
|
||||
g.set_joiner("land_type", lambda q: q.join(model.LandType))
|
||||
g.set_sorter("land_type", model.LandType.name)
|
||||
g.set_filter("land_type", model.LandType.name, label="Land Type Name")
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
||||
# land_type
|
||||
f.set_node("land_type", LandTypeRef(self.request))
|
||||
|
||||
def get_farmos_url(self, land):
|
||||
return self.app.get_farmos_url(f"/asset/{land.drupal_id}")
|
||||
|
||||
def get_xref_buttons(self, land_asset):
|
||||
buttons = super().get_xref_buttons(land_asset)
|
||||
|
||||
if land_asset.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_land_assets.view", uuid=land_asset.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
LandAssetView = kwargs.get("LandAssetView", base["LandAssetView"])
|
||||
LandAssetView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Master view for Land Types
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.land import LandType, LandAsset
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
|
||||
|
||||
class LandTypeView(WuttaFarmMasterView):
|
||||
"""
|
||||
Master view for Land Types
|
||||
"""
|
||||
|
||||
model_class = LandType
|
||||
route_prefix = "land_types"
|
||||
url_prefix = "/land-types"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"farmos_uuid",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
has_rows = True
|
||||
row_model_class = LandAsset
|
||||
rows_viewable = True
|
||||
|
||||
row_grid_columns = [
|
||||
"name",
|
||||
"is_location",
|
||||
"is_fixed",
|
||||
"active",
|
||||
]
|
||||
|
||||
rows_sort_defaults = "name"
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def get_xref_buttons(self, land_type):
|
||||
buttons = super().get_xref_buttons(land_type)
|
||||
|
||||
if land_type.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_land_types.view", uuid=land_type.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
def get_row_grid_data(self, land_type):
|
||||
model = self.app.model
|
||||
session = self.Session()
|
||||
return session.query(model.LandAsset).filter(
|
||||
model.LandAsset.land_type == land_type
|
||||
)
|
||||
|
||||
def configure_row_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_row_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def get_row_action_url_view(self, land_asset, i):
|
||||
return self.request.route_url("land_assets.view", uuid=land_asset.uuid)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
LandTypeView = kwargs.get("LandTypeView", base["LandTypeView"])
|
||||
LandTypeView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
431
src/wuttafarm/web/views/logs.py
Normal file
431
src/wuttafarm/web/views/logs.py
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
# -*- 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 views for Logs
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import colander
|
||||
from webhelpers2.html import tags, HTML
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDictEnum
|
||||
from wuttaweb.db import Session
|
||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from wuttafarm.db.model import LogType, Log
|
||||
from wuttafarm.web.forms.schema import AssetRefs, LogQuantityRefs, OwnerRefs
|
||||
from wuttafarm.util import get_log_type_enum
|
||||
|
||||
|
||||
class LogTypeView(WuttaFarmMasterView):
|
||||
"""
|
||||
Master view for Log Types
|
||||
"""
|
||||
|
||||
model_class = LogType
|
||||
route_prefix = "log_types"
|
||||
url_prefix = "/log-types"
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"description",
|
||||
]
|
||||
|
||||
sort_defaults = "name"
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"description",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
|
||||
def get_xref_buttons(self, log_type):
|
||||
buttons = super().get_xref_buttons(log_type)
|
||||
|
||||
if log_type.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_log_types.view", uuid=log_type.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
class LogMasterView(WuttaFarmMasterView):
|
||||
"""
|
||||
Base class for Asset master views
|
||||
"""
|
||||
|
||||
farmos_entity_type = "log"
|
||||
|
||||
labels = {
|
||||
"message": "Log Name",
|
||||
"locations": "Location",
|
||||
"quantities": "Quantity",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"message",
|
||||
"assets",
|
||||
"locations",
|
||||
"quantities",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
||||
filter_defaults = {
|
||||
"message": {"active": True, "verb": "contains"},
|
||||
"status": {"active": True, "verb": "not_equal", "value": "abandoned"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"message",
|
||||
"timestamp",
|
||||
"assets",
|
||||
"groups",
|
||||
"locations",
|
||||
"quantities",
|
||||
"notes",
|
||||
"status",
|
||||
"log_type",
|
||||
"owners",
|
||||
"is_movement",
|
||||
"is_group_assignment",
|
||||
"quick",
|
||||
"drupal_id",
|
||||
"farmos_uuid",
|
||||
]
|
||||
|
||||
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.Log:
|
||||
query = query.join(model.Log)
|
||||
return query
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
||||
# status
|
||||
g.set_enum("status", enum.LOG_STATUS)
|
||||
g.set_sorter("status", model.Log.status)
|
||||
g.set_filter(
|
||||
"status",
|
||||
model.Log.status,
|
||||
verbs=["equal", "not_equal"],
|
||||
choices=enum.LOG_STATUS,
|
||||
)
|
||||
|
||||
# drupal_id
|
||||
g.set_label("drupal_id", "ID", column_only=True)
|
||||
g.set_sorter("drupal_id", model.Log.drupal_id)
|
||||
g.set_filter("drupal_id", model.Log.drupal_id)
|
||||
|
||||
# timestamp
|
||||
g.set_renderer("timestamp", "date")
|
||||
g.set_link("timestamp")
|
||||
g.set_sorter("timestamp", model.Log.timestamp)
|
||||
g.set_filter("timestamp", model.Log.timestamp)
|
||||
|
||||
# message
|
||||
g.set_link("message")
|
||||
g.set_sorter("message", model.Log.message)
|
||||
g.set_filter("message", model.Log.message)
|
||||
|
||||
# assets
|
||||
g.set_renderer("assets", self.render_assets_for_grid)
|
||||
|
||||
# groups
|
||||
g.set_renderer("groups", self.render_assets_for_grid)
|
||||
|
||||
# locations
|
||||
g.set_renderer("locations", 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", model.Log.is_group_assignment)
|
||||
g.set_filter("is_group_assignment", model.Log.is_group_assignment)
|
||||
|
||||
# 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 = getattr(log, field)
|
||||
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for asset in assets:
|
||||
url = self.request.route_url(
|
||||
f"{asset.asset_type}_assets.view", uuid=asset.uuid
|
||||
)
|
||||
links.append(tags.link_to(str(asset), url))
|
||||
return ", ".join(links)
|
||||
|
||||
return ", ".join([str(a) for a in assets])
|
||||
|
||||
def render_quantities_for_grid(self, log, field, value):
|
||||
quantities = getattr(log, field) or []
|
||||
items = []
|
||||
for qty in quantities:
|
||||
items.append(HTML.tag("li", c=qty.render_as_text(self.config)))
|
||||
return HTML.tag("ul", c=items)
|
||||
|
||||
def render_owners_for_grid(self, log, field, value):
|
||||
|
||||
if self.farmos_style_grid_links:
|
||||
links = []
|
||||
for user in log.owners:
|
||||
url = self.request.route_url("users.view", uuid=user.uuid)
|
||||
links.append(tags.link_to(user.username, url))
|
||||
return ", ".join(links)
|
||||
|
||||
return ", ".join([user.username for user in log.owners])
|
||||
|
||||
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 configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
enum = self.app.enum
|
||||
session = self.Session()
|
||||
log = f.model_instance
|
||||
|
||||
# timestamp
|
||||
# TODO: the widget should be automatic (assn proxy field)
|
||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
||||
if self.creating:
|
||||
f.set_default("timestamp", self.app.make_utc())
|
||||
|
||||
# assets
|
||||
if self.creating or self.editing:
|
||||
f.remove("assets") # TODO: need to support this
|
||||
else:
|
||||
f.set_node("assets", AssetRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("assets", log.assets)
|
||||
|
||||
# groups
|
||||
if self.creating or self.editing:
|
||||
f.remove("groups") # TODO: need to support this
|
||||
else:
|
||||
f.set_node("groups", AssetRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("groups", log.groups)
|
||||
|
||||
# locations
|
||||
if self.creating or self.editing:
|
||||
f.remove("locations") # TODO: need to support this
|
||||
else:
|
||||
f.set_node("locations", AssetRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("locations", log.locations)
|
||||
|
||||
# log_type
|
||||
if self.creating:
|
||||
f.remove("log_type")
|
||||
else:
|
||||
f.set_node(
|
||||
"log_type",
|
||||
WuttaDictEnum(
|
||||
self.request, get_log_type_enum(self.config, session=session)
|
||||
),
|
||||
)
|
||||
f.set_readonly("log_type")
|
||||
|
||||
# quantities
|
||||
if self.creating or self.editing:
|
||||
f.remove("quantities") # TODO: need to support this
|
||||
else:
|
||||
f.set_node("quantities", LogQuantityRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("quantities", log.quantities)
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
# owners
|
||||
if self.creating or self.editing:
|
||||
f.remove("owners") # TODO: need to support this
|
||||
else:
|
||||
f.set_node("owners", OwnerRefs(self.request))
|
||||
# nb. must explicity declare value for non-standard field
|
||||
f.set_default("owners", log.owners)
|
||||
|
||||
# status
|
||||
f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS))
|
||||
|
||||
# is_movement
|
||||
f.set_node("is_movement", colander.Boolean())
|
||||
|
||||
# is_group_assignment
|
||||
f.set_node("is_group_assignment", colander.Boolean())
|
||||
|
||||
# quick
|
||||
f.set_readonly("quick") # TODO
|
||||
|
||||
def objectify(self, form):
|
||||
log = super().objectify(form)
|
||||
|
||||
if self.creating:
|
||||
model_class = self.get_model_class()
|
||||
log.log_type = self.get_farmos_log_type()
|
||||
|
||||
return log
|
||||
|
||||
def get_farmos_url(self, log):
|
||||
return self.app.get_farmos_url(f"/log/{log.drupal_id}")
|
||||
|
||||
def get_farmos_log_type(self):
|
||||
return self.model_class.__wutta_hint__["farmos_log_type"]
|
||||
|
||||
def get_xref_buttons(self, log):
|
||||
buttons = super().get_xref_buttons(log)
|
||||
|
||||
if log.farmos_uuid:
|
||||
log_type = self.get_farmos_log_type()
|
||||
route = f"farmos_logs_{log_type}.view"
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(route, uuid=log.farmos_uuid),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
def get_version_joins(self):
|
||||
"""
|
||||
We override this to declare the relationship between the
|
||||
view's data model (which is some type of log table) and the
|
||||
canonical ``Log`` model, so the revision history views include
|
||||
transactions which reference either version table.
|
||||
|
||||
See also parent method,
|
||||
:meth:`~wuttaweb:wuttaweb.views.master.MasterView.get_version_joins()`
|
||||
"""
|
||||
model = self.app.model
|
||||
return super().get_version_joins() + [
|
||||
model.Log,
|
||||
(model.LogAsset, "log_uuid", "uuid"),
|
||||
]
|
||||
|
||||
|
||||
class AllLogView(LogMasterView):
|
||||
"""
|
||||
Master view for All Logs
|
||||
"""
|
||||
|
||||
model_class = Log
|
||||
route_prefix = "log"
|
||||
url_prefix = "/logs"
|
||||
|
||||
farmos_refurl_path = "/logs"
|
||||
|
||||
viewable = False
|
||||
creatable = False
|
||||
editable = False
|
||||
deletable = False
|
||||
model_is_versioned = False
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"message",
|
||||
"log_type",
|
||||
"assets",
|
||||
"locations",
|
||||
"quantities",
|
||||
"groups",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
session = self.Session()
|
||||
|
||||
# log_type
|
||||
g.set_enum("log_type", get_log_type_enum(self.config, session=session))
|
||||
|
||||
# view action links to final log record
|
||||
def log_url(log, i):
|
||||
return self.request.route_url(f"logs_{log.log_type}.view", uuid=log.uuid)
|
||||
|
||||
g.add_action("view", icon="eye", url=log_url)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
LogTypeView = kwargs.get("LogTypeView", base["LogTypeView"])
|
||||
LogTypeView.defaults(config)
|
||||
|
||||
AllLogView = kwargs.get("AllLogView", base["AllLogView"])
|
||||
AllLogView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
@ -23,76 +23,22 @@
|
|||
Master view for Activity Logs
|
||||
"""
|
||||
|
||||
from wuttafarm.db.model.logs import ActivityLog
|
||||
from wuttafarm.web.views import WuttaFarmMasterView
|
||||
from wuttafarm.web.views.logs import LogMasterView
|
||||
from wuttafarm.db.model import ActivityLog
|
||||
|
||||
|
||||
class ActivityLogView(WuttaFarmMasterView):
|
||||
class ActivityLogView(LogMasterView):
|
||||
"""
|
||||
Master view for Activity Logs
|
||||
"""
|
||||
|
||||
model_class = ActivityLog
|
||||
route_prefix = "activity_logs"
|
||||
route_prefix = "logs_activity"
|
||||
url_prefix = "/logs/activity"
|
||||
|
||||
farmos_bundle = "activity"
|
||||
farmos_refurl_path = "/logs/activity"
|
||||
|
||||
grid_columns = [
|
||||
"message",
|
||||
"timestamp",
|
||||
"status",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
||||
filter_defaults = {
|
||||
"message": {"active": True, "verb": "contains"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"message",
|
||||
"timestamp",
|
||||
"status",
|
||||
"notes",
|
||||
"farmos_uuid",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
|
||||
# message
|
||||
g.set_link("message")
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
def get_farmos_url(self, log):
|
||||
return self.app.get_farmos_url(f"/log/{log.drupal_id}")
|
||||
|
||||
def get_xref_buttons(self, log):
|
||||
buttons = super().get_xref_buttons(log)
|
||||
|
||||
if log.farmos_uuid:
|
||||
buttons.append(
|
||||
self.make_button(
|
||||
"View farmOS record",
|
||||
primary=True,
|
||||
url=self.request.route_url(
|
||||
"farmos_logs_activity.view", uuid=log.farmos_uuid
|
||||
),
|
||||
icon_left="eye",
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue