feat: overhaul farmOS log views; add Eggs quick form
probably a few other changes...i'm tired and need a savepoint
This commit is contained in:
parent
ad6ac13d50
commit
1a6870b8fe
15 changed files with 914 additions and 36 deletions
|
|
@ -55,6 +55,34 @@ class AnimalTypeRef(ObjectRef):
|
|||
return self.request.route_url("animal_types.view", uuid=animal_type.uuid)
|
||||
|
||||
|
||||
class LogQuick(WuttaSet):
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if appstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
return json.dumps(appstruct)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import LogQuickWidget
|
||||
|
||||
return LogQuickWidget(**kwargs)
|
||||
|
||||
|
||||
class FarmOSUnitRef(colander.SchemaType):
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if appstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
return json.dumps(appstruct)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import FarmOSUnitRefWidget
|
||||
|
||||
return FarmOSUnitRefWidget(**kwargs)
|
||||
|
||||
|
||||
class FarmOSRef(colander.SchemaType):
|
||||
|
||||
def __init__(self, request, route_prefix, *args, **kwargs):
|
||||
|
|
@ -114,6 +142,20 @@ class FarmOSRefs(WuttaSet):
|
|||
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):
|
||||
|
|
@ -128,6 +170,20 @@ class FarmOSLocationRefs(WuttaSet):
|
|||
return FarmOSLocationRefsWidget(self.request, **kwargs)
|
||||
|
||||
|
||||
class FarmOSQuantityRefs(WuttaSet):
|
||||
|
||||
def serialize(self, node, appstruct):
|
||||
if appstruct is colander.null:
|
||||
return colander.null
|
||||
|
||||
return json.dumps(appstruct)
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
from wuttafarm.web.forms.widgets import FarmOSQuantityRefsWidget
|
||||
|
||||
return FarmOSQuantityRefsWidget(**kwargs)
|
||||
|
||||
|
||||
class AnimalTypeType(colander.SchemaType):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ from webhelpers2.html import HTML, tags
|
|||
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget
|
||||
from wuttaweb.db import Session
|
||||
|
||||
from wuttafarm.web.util import render_quantity_objects
|
||||
|
||||
|
||||
class ImageWidget(Widget):
|
||||
"""
|
||||
|
|
@ -54,6 +56,26 @@ class ImageWidget(Widget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class LogQuickWidget(Widget):
|
||||
"""
|
||||
Widget to display an image URL for a record.
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
if cstruct in (colander.null, None):
|
||||
return HTML.tag("span")
|
||||
|
||||
items = []
|
||||
for quick in json.loads(cstruct):
|
||||
items.append(HTML.tag("li", c=quick))
|
||||
return HTML.tag("ul", c=items)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSRefWidget(SelectWidget):
|
||||
"""
|
||||
Generic widget to display "any reference field" - as a link to
|
||||
|
|
@ -111,6 +133,33 @@ class FarmOSRefsWidget(Widget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSAssetRefsWidget(Widget):
|
||||
"""
|
||||
Widget to display a "Assets" field for an asset.
|
||||
"""
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
if cstruct in (colander.null, None):
|
||||
return HTML.tag("span")
|
||||
|
||||
assets = []
|
||||
for asset in json.loads(cstruct):
|
||||
url = self.request.route_url(
|
||||
f"farmos_{asset['asset_type']}_assets.view", uuid=asset["uuid"]
|
||||
)
|
||||
assets.append(HTML.tag("li", c=tags.link_to(asset["name"], url)))
|
||||
|
||||
return HTML.tag("ul", c=assets)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSLocationRefsWidget(Widget):
|
||||
"""
|
||||
Widget to display a "Locations" field for an asset.
|
||||
|
|
@ -139,6 +188,40 @@ class FarmOSLocationRefsWidget(Widget):
|
|||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSQuantityRefsWidget(Widget):
|
||||
"""
|
||||
Widget to display a "Quantities" field for a log.
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
if cstruct in (colander.null, None):
|
||||
return HTML.tag("span")
|
||||
|
||||
quantities = json.loads(cstruct)
|
||||
return render_quantity_objects(quantities)
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class FarmOSUnitRefWidget(Widget):
|
||||
"""
|
||||
Widget to display a "Units" field for a quantity.
|
||||
"""
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
readonly = kw.get("readonly", self.readonly)
|
||||
if readonly:
|
||||
if cstruct in (colander.null, None):
|
||||
return HTML.tag("span")
|
||||
|
||||
unit = json.loads(cstruct)
|
||||
return unit["name"]
|
||||
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
||||
|
||||
class AnimalTypeWidget(Widget):
|
||||
"""
|
||||
Widget to display an "animal type" field.
|
||||
|
|
|
|||
|
|
@ -35,29 +35,48 @@ class WuttaFarmMenuHandler(base.MenuHandler):
|
|||
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),
|
||||
self.make_admin_menu(request, include_people=True),
|
||||
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),
|
||||
self.make_admin_menu(request, include_people=True),
|
||||
admin_menu,
|
||||
]
|
||||
|
||||
else: # FARMOS_INTEGRATION_MODE_NONE
|
||||
return [
|
||||
quick_menu,
|
||||
self.make_asset_menu(request),
|
||||
self.make_log_menu(request),
|
||||
self.make_admin_menu(request, include_people=True),
|
||||
admin_menu,
|
||||
]
|
||||
|
||||
def make_quick_menu(self, request):
|
||||
return {
|
||||
"title": "Quick",
|
||||
"type": "menu",
|
||||
"items": [
|
||||
{
|
||||
"title": "Eggs",
|
||||
"route": "quick.eggs",
|
||||
# "perm": "assets.list",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def make_asset_menu(self, request):
|
||||
return {
|
||||
"title": "Assets",
|
||||
|
|
|
|||
14
src/wuttafarm/web/templates/quick/form.mako
Normal file
14
src/wuttafarm/web/templates/quick/form.mako
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<%inherit file="/form.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » ${form_title}</%def>
|
||||
|
||||
<%def name="content_title()">${form_title}</%def>
|
||||
|
||||
<%def name="render_form_tag()">
|
||||
|
||||
<p class="block">
|
||||
${help_text}
|
||||
</p>
|
||||
|
||||
${parent.render_form_tag()}
|
||||
</%def>
|
||||
|
|
@ -23,6 +23,8 @@
|
|||
Misc. utilities for web app
|
||||
"""
|
||||
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
|
||||
def save_farmos_oauth2_token(request, token):
|
||||
"""
|
||||
|
|
@ -42,3 +44,18 @@ def save_farmos_oauth2_token(request, 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}"
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ def includeme(config):
|
|||
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
|
||||
if mode != enum.FARMOS_INTEGRATION_MODE_NONE:
|
||||
config.include("wuttafarm.web.views.farmos")
|
||||
|
|
|
|||
|
|
@ -119,16 +119,6 @@ class AssetMasterView(FarmOSMasterView):
|
|||
return tags.image(url, f"thumbnail for {self.get_model_title()}")
|
||||
return None
|
||||
|
||||
def render_owners_for_grid(self, asset, 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 render_locations_for_grid(self, asset, field, value):
|
||||
locations = []
|
||||
for location in value:
|
||||
|
|
@ -151,15 +141,14 @@ class AssetMasterView(FarmOSMasterView):
|
|||
return {"asset_type", "location", "owner", "image"}
|
||||
|
||||
def get_instance(self):
|
||||
asset = self.farmos_client.resource.get_id(
|
||||
"asset",
|
||||
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 = asset
|
||||
included = {obj["id"]: obj for obj in asset.get("included", [])}
|
||||
return self.normalize_asset(asset["data"], included)
|
||||
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"]
|
||||
|
|
|
|||
|
|
@ -26,11 +26,27 @@ View for farmOS Harvest Logs
|
|||
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.grids import (
|
||||
ResourceData,
|
||||
SimpleSorter,
|
||||
StringFilter,
|
||||
IntegerFilter,
|
||||
DateTimeFilter,
|
||||
NullableBooleanFilter,
|
||||
)
|
||||
from wuttafarm.web.forms.schema import (
|
||||
FarmOSQuantityRefs,
|
||||
FarmOSAssetRefs,
|
||||
FarmOSRefs,
|
||||
LogQuick,
|
||||
)
|
||||
from wuttafarm.web.util import render_quantity_objects
|
||||
|
||||
|
||||
class LogMasterView(FarmOSMasterView):
|
||||
|
|
@ -39,48 +55,183 @@ class LogMasterView(FarmOSMasterView):
|
|||
"""
|
||||
|
||||
farmos_log_type = None
|
||||
filterable = True
|
||||
sort_on_backend = True
|
||||
|
||||
_farmos_units = None
|
||||
_farmos_measures = None
|
||||
|
||||
labels = {
|
||||
"name": "Log Name",
|
||||
"log_type_name": "Log Type",
|
||||
"quantities": "Quantity",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"name",
|
||||
"timestamp",
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"quantities",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
||||
filter_defaults = {
|
||||
"name": {"active": True, "verb": "contains"},
|
||||
"status": {"active": True, "verb": "not_equal", "value": "abandoned"},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
"name",
|
||||
"timestamp",
|
||||
"status",
|
||||
"assets",
|
||||
"quantities",
|
||||
"notes",
|
||||
"status",
|
||||
"log_type_name",
|
||||
"owners",
|
||||
"quick",
|
||||
"drupal_id",
|
||||
]
|
||||
|
||||
def get_grid_data(self, columns=None, session=None):
|
||||
result = self.farmos_client.log.get(self.farmos_log_type)
|
||||
return [self.normalize_log(l) for l in result["data"]]
|
||||
def get_farmos_api_includes(self):
|
||||
return {"log_type", "quantity", "asset", "owner"}
|
||||
|
||||
def get_grid_data(self, **kwargs):
|
||||
return ResourceData(
|
||||
self.config,
|
||||
self.farmos_client,
|
||||
f"log--{self.farmos_log_type}",
|
||||
include=",".join(self.get_farmos_api_includes()),
|
||||
normalizer=self.normalize_log,
|
||||
)
|
||||
|
||||
def configure_grid(self, grid):
|
||||
g = grid
|
||||
super().configure_grid(g)
|
||||
enum = self.app.enum
|
||||
|
||||
# status
|
||||
g.set_enum("status", enum.LOG_STATUS)
|
||||
g.set_sorter("status", SimpleSorter("status"))
|
||||
g.set_filter(
|
||||
"status",
|
||||
StringFilter,
|
||||
choices=enum.LOG_STATUS,
|
||||
verbs=["equal", "not_equal"],
|
||||
)
|
||||
|
||||
# drupal_id
|
||||
g.set_label("drupal_id", "ID", column_only=True)
|
||||
g.set_sorter("drupal_id", SimpleSorter("drupal_internal__id"))
|
||||
g.set_filter("drupal_id", IntegerFilter, path="drupal_internal__id")
|
||||
|
||||
# timestamp
|
||||
g.set_renderer("timestamp", "date")
|
||||
g.set_link("timestamp")
|
||||
g.set_sorter("timestamp", SimpleSorter("timestamp"))
|
||||
g.set_filter("timestamp", DateTimeFilter)
|
||||
|
||||
# name
|
||||
g.set_link("name")
|
||||
g.set_searchable("name")
|
||||
g.set_sorter("name", SimpleSorter("name"))
|
||||
g.set_filter("name", StringFilter)
|
||||
|
||||
# timestamp
|
||||
g.set_renderer("timestamp", "datetime")
|
||||
# assets
|
||||
g.set_renderer("assets", self.render_assets_for_grid)
|
||||
|
||||
# quantities
|
||||
g.set_renderer("quantities", self.render_quantities_for_grid)
|
||||
|
||||
# is_group_assignment
|
||||
g.set_renderer("is_group_assignment", "boolean")
|
||||
g.set_sorter("is_group_assignment", SimpleSorter("is_group_assignment"))
|
||||
g.set_filter("is_group_assignment", NullableBooleanFilter)
|
||||
|
||||
# owners
|
||||
g.set_label("owners", "Owner")
|
||||
g.set_renderer("owners", self.render_owners_for_grid)
|
||||
|
||||
def render_assets_for_grid(self, log, field, value):
|
||||
assets = []
|
||||
for asset in value:
|
||||
if self.farmos_style_grid_links:
|
||||
url = self.request.route_url(
|
||||
f"farmos_{asset['asset_type']}_assets.view", uuid=asset["uuid"]
|
||||
)
|
||||
assets.append(tags.link_to(asset["name"], url))
|
||||
else:
|
||||
assets.append(asset["name"])
|
||||
return ", ".join(assets)
|
||||
|
||||
def render_quantities_for_grid(self, log, field, value):
|
||||
if not value:
|
||||
return None
|
||||
return render_quantity_objects(value)
|
||||
|
||||
def grid_row_class(self, log, data, i):
|
||||
if log["status"] == "pending":
|
||||
return "has-background-warning"
|
||||
if log["status"] == "abandoned":
|
||||
return "has-background-danger"
|
||||
return None
|
||||
|
||||
def get_instance(self):
|
||||
log = self.farmos_client.log.get_id(
|
||||
self.farmos_log_type, self.request.matchdict["uuid"]
|
||||
result = self.farmos_client.log.get_id(
|
||||
self.farmos_log_type,
|
||||
self.request.matchdict["uuid"],
|
||||
params={"include": ",".join(self.get_farmos_api_includes())},
|
||||
)
|
||||
self.raw_json = log
|
||||
return self.normalize_log(log["data"])
|
||||
self.raw_json = result
|
||||
included = {obj["id"]: obj for obj in result.get("included", [])}
|
||||
return self.normalize_log(result["data"], included)
|
||||
|
||||
def get_instance_title(self, log):
|
||||
return log["name"]
|
||||
|
||||
def normalize_log(self, log):
|
||||
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 get_farmos_unit(self, uuid):
|
||||
units = self.get_farmos_units()
|
||||
return units[uuid]
|
||||
|
||||
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 normalize_log(self, log, included):
|
||||
|
||||
if timestamp := log["attributes"]["timestamp"]:
|
||||
timestamp = datetime.datetime.fromisoformat(timestamp)
|
||||
|
|
@ -89,26 +240,126 @@ class LogMasterView(FarmOSMasterView):
|
|||
if notes := log["attributes"]["notes"]:
|
||||
notes = notes["value"]
|
||||
|
||||
log_type_object = {}
|
||||
log_type_name = None
|
||||
asset_objects = []
|
||||
quantity_objects = []
|
||||
owner_objects = []
|
||||
if relationships := log.get("relationships"):
|
||||
|
||||
if log_type := relationships.get("log_type"):
|
||||
log_type = included[log_type["data"]["id"]]
|
||||
log_type_object = {
|
||||
"uuid": log_type["id"],
|
||||
"name": log_type["attributes"]["label"],
|
||||
}
|
||||
log_type_name = log_type_object["name"]
|
||||
|
||||
if assets := relationships.get("asset"):
|
||||
for asset in assets["data"]:
|
||||
asset = included[asset["id"]]
|
||||
attrs = asset["attributes"]
|
||||
rels = asset["relationships"]
|
||||
asset_objects.append(
|
||||
{
|
||||
"uuid": asset["id"],
|
||||
"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_type": asset["type"].split("--")[1],
|
||||
}
|
||||
)
|
||||
|
||||
if quantities := relationships.get("quantity"):
|
||||
for quantity in quantities["data"]:
|
||||
quantity = included[quantity["id"]]
|
||||
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 = included[user["id"]]
|
||||
owner_objects.append(
|
||||
{
|
||||
"uuid": user["id"],
|
||||
"name": user["attributes"]["name"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"uuid": log["id"],
|
||||
"drupal_id": log["attributes"]["drupal_internal__id"],
|
||||
"log_type": log_type_object,
|
||||
"log_type_name": log_type_name,
|
||||
"name": log["attributes"]["name"],
|
||||
"timestamp": timestamp,
|
||||
"assets": asset_objects,
|
||||
"quantities": quantity_objects,
|
||||
"is_group_assignment": log["attributes"]["is_group_assignment"],
|
||||
"quick": log["attributes"]["quick"],
|
||||
"status": log["attributes"]["status"],
|
||||
"notes": notes or colander.null,
|
||||
"owners": owner_objects,
|
||||
}
|
||||
|
||||
def configure_form(self, form):
|
||||
f = form
|
||||
super().configure_form(f)
|
||||
enum = self.app.enum
|
||||
log = f.model_instance
|
||||
|
||||
# timestamp
|
||||
f.set_node("timestamp", WuttaDateTime())
|
||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
||||
|
||||
# assets
|
||||
f.set_node("assets", FarmOSAssetRefs(self.request))
|
||||
|
||||
# quantities
|
||||
f.set_node("quantities", FarmOSQuantityRefs(self.request))
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
|
||||
# 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()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,17 @@ class HarvestLogView(LogMasterView):
|
|||
farmos_log_type = "harvest"
|
||||
farmos_refurl_path = "/logs/harvest"
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"name",
|
||||
"assets",
|
||||
"quantities",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import json
|
|||
|
||||
import colander
|
||||
import markdown
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import WuttaDateTime
|
||||
|
|
@ -99,6 +100,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:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ 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 FarmOSRef
|
||||
from wuttafarm.web.forms.schema import FarmOSUnitRef
|
||||
|
||||
|
||||
class QuantityTypeView(FarmOSMasterView):
|
||||
|
|
@ -220,7 +220,7 @@ class QuantityMasterView(FarmOSMasterView):
|
|||
f.set_widget("changed", WuttaDateTimeWidget(self.request))
|
||||
|
||||
# units
|
||||
f.set_node("units", FarmOSRef(self.request, "farmos_units"))
|
||||
f.set_node("units", FarmOSUnitRef())
|
||||
|
||||
|
||||
class StandardQuantityView(QuantityMasterView):
|
||||
|
|
|
|||
|
|
@ -175,15 +175,21 @@ class LogMasterView(WuttaFarmMasterView):
|
|||
Base class for Asset master views
|
||||
"""
|
||||
|
||||
labels = {
|
||||
"message": "Log Name",
|
||||
"owners": "Owner",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
"status",
|
||||
"drupal_id",
|
||||
"timestamp",
|
||||
"message",
|
||||
"assets",
|
||||
"location",
|
||||
# "location",
|
||||
"quantity",
|
||||
"is_group_assignment",
|
||||
"owners",
|
||||
]
|
||||
|
||||
sort_defaults = ("timestamp", "desc")
|
||||
|
|
|
|||
30
src/wuttafarm/web/views/quick/__init__.py
Normal file
30
src/wuttafarm/web/views/quick/__init__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Quick Form views for farmOS
|
||||
"""
|
||||
|
||||
from .base import QuickFormView
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include("wuttafarm.web.views.quick.eggs")
|
||||
155
src/wuttafarm/web/views/quick/base.py
Normal file
155
src/wuttafarm/web/views/quick/base.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Base class for Quick Form views
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from pyramid.renderers import render_to_response
|
||||
|
||||
from wuttaweb.views import View
|
||||
|
||||
from wuttafarm.web.util import save_farmos_oauth2_token
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuickFormView(View):
|
||||
"""
|
||||
Base class for quick form views.
|
||||
"""
|
||||
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.farmos_client = self.get_farmos_client()
|
||||
self.farmos_4x = self.app.is_farmos_4x(self.farmos_client)
|
||||
|
||||
@classmethod
|
||||
def get_route_slug(cls):
|
||||
return cls.route_slug
|
||||
|
||||
@classmethod
|
||||
def get_url_slug(cls):
|
||||
return cls.url_slug
|
||||
|
||||
@classmethod
|
||||
def get_form_title(cls):
|
||||
return cls.form_title
|
||||
|
||||
def __call__(self):
|
||||
form = self.make_quick_form()
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
result = self.save_quick_form(form)
|
||||
except Exception as err:
|
||||
log.warning("failed to save 'edit' form", exc_info=True)
|
||||
self.request.session.flash(
|
||||
f"Save failed: {self.app.render_error(err)}", "error"
|
||||
)
|
||||
else:
|
||||
return self.redirect_after_save(result)
|
||||
|
||||
return self.render_to_response({"form": form})
|
||||
|
||||
def make_quick_form(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def save_quick_form(self, form):
|
||||
raise NotImplementedError
|
||||
|
||||
def redirect_after_save(self, result):
|
||||
return self.redirect(self.request.current_route_url())
|
||||
|
||||
def render_to_response(self, context):
|
||||
|
||||
defaults = {
|
||||
"index_title": "Quick Form",
|
||||
"form_title": self.get_form_title(),
|
||||
"help_text": self.__doc__.strip(),
|
||||
}
|
||||
|
||||
defaults.update(context)
|
||||
context = defaults
|
||||
|
||||
# supplement context further if needed
|
||||
context = self.get_template_context(context)
|
||||
|
||||
page_templates = self.get_page_templates()
|
||||
mako_path = page_templates[0]
|
||||
try:
|
||||
render_to_response(mako_path, context, request=self.request)
|
||||
except IOError:
|
||||
|
||||
# try one or more fallback templates
|
||||
for fallback in page_templates[1:]:
|
||||
try:
|
||||
return render_to_response(fallback, context, request=self.request)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
# if we made it all the way here, then we found no
|
||||
# templates at all, in which case re-attempt the first and
|
||||
# let that error raise on up
|
||||
return render_to_response(mako_path, context, request=self.request)
|
||||
|
||||
def get_page_templates(self):
|
||||
route_slug = self.get_route_slug()
|
||||
page_templates = [f"/quick/{route_slug}.mako"]
|
||||
page_templates.extend(self.get_fallback_templates())
|
||||
return page_templates
|
||||
|
||||
def get_fallback_templates(self):
|
||||
return ["/quick/form.mako"]
|
||||
|
||||
def get_template_context(self, context):
|
||||
return context
|
||||
|
||||
def get_farmos_client(self):
|
||||
token = self.request.session.get("farmos.oauth2.token")
|
||||
if not token:
|
||||
raise self.forbidden()
|
||||
|
||||
# nb. must give a *copy* of the token to farmOS client, since
|
||||
# it will mutate it in-place and we don't want that to happen
|
||||
# for our original copy in the user session. (otherwise the
|
||||
# auto-refresh will not work correctly for subsequent calls.)
|
||||
token = dict(token)
|
||||
|
||||
def token_updater(token):
|
||||
save_farmos_oauth2_token(self.request, token)
|
||||
|
||||
return self.app.get_farmos_client(token=token, token_updater=token_updater)
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _defaults(cls, config):
|
||||
route_slug = cls.get_route_slug()
|
||||
url_slug = cls.get_url_slug()
|
||||
|
||||
config.add_route(f"quick.{route_slug}", f"/quick/{url_slug}")
|
||||
config.add_view(cls, route_name=f"quick.{route_slug}")
|
||||
232
src/wuttafarm/web/views/quick/eggs.py
Normal file
232
src/wuttafarm/web/views/quick/eggs.py
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# WuttaFarm --Web app to integrate with and extend farmOS
|
||||
# Copyright © 2026 Lance Edgar
|
||||
#
|
||||
# This file is part of WuttaFarm.
|
||||
#
|
||||
# WuttaFarm is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# WuttaFarm is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# WuttaFarm. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Quick Form for "Eggs"
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import colander
|
||||
from deform.widget import SelectWidget
|
||||
|
||||
from farmOS.subrequests import Action, Subrequest, SubrequestsBlueprint, Format
|
||||
|
||||
from wuttaweb.forms.schema import WuttaDateTime
|
||||
from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||
|
||||
from wuttafarm.web.views.quick import QuickFormView
|
||||
|
||||
|
||||
class EggsQuickForm(QuickFormView):
|
||||
"""
|
||||
Use this form to record an egg harvest. A harvest log will be
|
||||
created with standard details filled in.
|
||||
"""
|
||||
|
||||
form_title = "Eggs"
|
||||
route_slug = "eggs"
|
||||
url_slug = "eggs"
|
||||
|
||||
_layer_assets = None
|
||||
|
||||
def make_quick_form(self):
|
||||
f = self.make_form(
|
||||
fields=[
|
||||
"timestamp",
|
||||
"count",
|
||||
"asset",
|
||||
"notes",
|
||||
],
|
||||
labels={
|
||||
"timestamp": "Date",
|
||||
"count": "Quantity",
|
||||
"asset": "Layer Asset",
|
||||
},
|
||||
)
|
||||
|
||||
# timestamp
|
||||
f.set_node("timestamp", WuttaDateTime())
|
||||
f.set_widget("timestamp", WuttaDateTimeWidget(self.request))
|
||||
f.set_default("timestamp", self.app.make_utc())
|
||||
|
||||
# count
|
||||
f.set_node("count", colander.Integer())
|
||||
|
||||
# asset
|
||||
assets = self.get_layer_assets()
|
||||
values = [(a["uuid"], a["name"]) for a in assets]
|
||||
f.set_widget("asset", SelectWidget(values=values))
|
||||
if len(assets) == 1:
|
||||
f.set_default("asset", assets[0]["uuid"])
|
||||
|
||||
# notes
|
||||
f.set_widget("notes", "notes")
|
||||
f.set_required("notes", False)
|
||||
|
||||
return f
|
||||
|
||||
def get_layer_assets(self):
|
||||
if self._layer_assets is not None:
|
||||
return self._layer_assets
|
||||
|
||||
assets = []
|
||||
params = {
|
||||
"filter[produces_eggs]": 1,
|
||||
"sort": "name",
|
||||
}
|
||||
|
||||
def normalize(asset):
|
||||
return {
|
||||
"uuid": asset["id"],
|
||||
"name": asset["attributes"]["name"],
|
||||
"type": asset["type"],
|
||||
}
|
||||
|
||||
result = self.farmos_client.asset.get("animal", params=params)
|
||||
assets.extend([normalize(a) for a in result["data"]])
|
||||
|
||||
result = self.farmos_client.asset.get("group", params=params)
|
||||
assets.extend([normalize(a) for a in result["data"]])
|
||||
|
||||
assets.sort(key=lambda a: a["name"])
|
||||
self._layer_assets = assets
|
||||
return assets
|
||||
|
||||
def save_quick_form(self, form):
|
||||
data = form.validated
|
||||
|
||||
assets = self.get_layer_assets()
|
||||
assets = {a["uuid"]: a for a in assets}
|
||||
asset = assets[data["asset"]]
|
||||
|
||||
# TODO: make this configurable?
|
||||
unit_name = "egg(s)"
|
||||
|
||||
unit = {"data": {"type": "taxonomy_term--unit"}}
|
||||
new_unit = None
|
||||
|
||||
result = self.farmos_client.resource.get(
|
||||
"taxonomy_term",
|
||||
"unit",
|
||||
params={
|
||||
"filter[name]": unit_name,
|
||||
},
|
||||
)
|
||||
if result["data"]:
|
||||
unit["data"]["id"] = result["data"][0]["id"]
|
||||
else:
|
||||
payload = dict(unit)
|
||||
payload["data"]["attributes"] = {"name": unit_name}
|
||||
new_unit = Subrequest(
|
||||
action=Action.create,
|
||||
requestId="create-unit",
|
||||
endpoint="api/taxonomy_term/unit",
|
||||
body=payload,
|
||||
)
|
||||
unit["data"]["id"] = "{{create-unit.body@$.data.id}}"
|
||||
|
||||
quantity = {
|
||||
"data": {
|
||||
"type": "quantity--standard",
|
||||
"attributes": {
|
||||
"measure": "count",
|
||||
"value": {
|
||||
"numerator": data["count"],
|
||||
"denominator": 1,
|
||||
},
|
||||
},
|
||||
"relationships": {
|
||||
"units": unit,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
kw = {}
|
||||
if new_unit:
|
||||
kw["waitFor"] = ["create-unit"]
|
||||
new_quantity = Subrequest(
|
||||
action=Action.create,
|
||||
requestId="create-quantity",
|
||||
endpoint="api/quantity/standard",
|
||||
body=quantity,
|
||||
**kw,
|
||||
)
|
||||
|
||||
notes = None
|
||||
if data["notes"]:
|
||||
notes = {"value": data["notes"]}
|
||||
|
||||
log = {
|
||||
"data": {
|
||||
"type": "log--harvest",
|
||||
"attributes": {
|
||||
"name": f"Collected {data['count']} {unit_name}",
|
||||
"notes": notes,
|
||||
"quick": ["eggs"],
|
||||
},
|
||||
"relationships": {
|
||||
"asset": {
|
||||
"data": [
|
||||
{
|
||||
"id": asset["uuid"],
|
||||
"type": asset["type"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"quantity": {
|
||||
"data": [
|
||||
{
|
||||
"id": "{{create-quantity.body@$.data.id}}",
|
||||
"type": "quantity--standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
new_log = Subrequest(
|
||||
action=Action.create,
|
||||
requestId="create-log",
|
||||
waitFor=["create-quantity"],
|
||||
endpoint="api/log/harvest",
|
||||
body=log,
|
||||
)
|
||||
|
||||
blueprints = [new_quantity, new_log]
|
||||
if new_unit:
|
||||
blueprints.insert(0, new_unit)
|
||||
blueprint = SubrequestsBlueprint.parse_obj(blueprints)
|
||||
response = self.farmos_client.subrequests.send(blueprint, format=Format.json)
|
||||
result = json.loads(response["create-log#body{0}"]["body"])
|
||||
return result
|
||||
|
||||
def redirect_after_save(self, result):
|
||||
return self.redirect(
|
||||
self.request.route_url(
|
||||
"farmos_logs_harvest.view", uuid=result["data"]["id"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
EggsQuickForm.defaults(config)
|
||||
Loading…
Add table
Add a link
Reference in a new issue