wuttafarm/src/wuttafarm/web/views/plants.py

483 lines
14 KiB
Python

# -*- 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 Plants
"""
from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.util import get_form_data
from wuttafarm.db.model import PlantType, Season, PlantAsset
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
from wuttafarm.web.forms.schema import PlantTypeRefs, SeasonRefs
from wuttafarm.web.forms.widgets import ImageWidget
from wuttafarm.web.util import get_farmos_client_for_user
class PlantTypeView(AssetTypeMasterView):
"""
Master view for Plant Types
"""
model_class = PlantType
route_prefix = "plant_types"
url_prefix = "/plant-types"
farmos_entity_type = "taxonomy_term"
farmos_bundle = "plant_type"
farmos_refurl_path = "/admin/structure/taxonomy/manage/plant_type/overview"
grid_columns = [
"name",
"description",
]
sort_defaults = "name"
filter_defaults = {
"name": {"active": True, "verb": "contains"},
}
form_fields = [
"name",
"description",
"drupal_id",
"farmos_uuid",
]
has_rows = True
row_model_class = PlantAsset
rows_viewable = True
row_grid_columns = [
"asset_name",
"archived",
]
rows_sort_defaults = "asset_name"
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# name
g.set_link("name")
def get_farmos_url(self, plant_type):
return self.app.get_farmos_url(f"/taxonomy/term/{plant_type.drupal_id}")
def get_xref_buttons(self, plant_type):
buttons = super().get_xref_buttons(plant_type)
if plant_type.farmos_uuid:
buttons.append(
self.make_button(
"View farmOS record",
primary=True,
url=self.request.route_url(
"farmos_plant_types.view", uuid=plant_type.farmos_uuid
),
icon_left="eye",
)
)
return buttons
def delete(self):
plant_type = self.get_instance()
if plant_type._plant_assets:
self.request.session.flash(
"Cannot delete plant type which is still referenced by plant assets.",
"warning",
)
url = self.get_action_url("view", plant_type)
return self.redirect(self.request.get_referrer(default=url))
return super().delete()
def get_row_grid_data(self, plant_type):
model = self.app.model
session = self.Session()
return (
session.query(model.PlantAsset)
.join(model.Asset)
.outerjoin(model.PlantAssetPlantType)
.filter(model.PlantAssetPlantType.plant_type == plant_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)
# 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, plant, i):
return self.request.route_url("plant_assets.view", uuid=plant.uuid)
def ajax_create(self):
"""
AJAX view to create a new plant 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"}
plant_type = model.PlantType(name=name)
session.add(plant_type)
session.flush()
if self.app.is_farmos_mirror():
client = get_farmos_client_for_user(self.request)
self.app.auto_sync_to_farmos(plant_type, client=client)
return {
"uuid": plant_type.uuid.hex,
"name": plant_type.name,
"farmos_uuid": plant_type.farmos_uuid.hex,
"drupal_id": plant_type.drupal_id,
}
@classmethod
def defaults(cls, config):
""" """
cls._defaults(config)
cls._plant_type_defaults(config)
@classmethod
def _plant_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 SeasonView(AssetTypeMasterView):
"""
Master view for Seasons
"""
model_class = Season
route_prefix = "seasons"
url_prefix = "/seasons"
farmos_entity_type = "taxonomy_term"
farmos_bundle = "season"
farmos_refurl_path = "/admin/structure/taxonomy/manage/season/overview"
grid_columns = [
"name",
"description",
]
sort_defaults = "name"
filter_defaults = {
"name": {"active": True, "verb": "contains"},
}
form_fields = [
"name",
"description",
"drupal_id",
"farmos_uuid",
]
has_rows = True
row_model_class = PlantAsset
rows_viewable = True
row_grid_columns = [
"asset_name",
"archived",
]
rows_sort_defaults = "asset_name"
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# name
g.set_link("name")
def get_farmos_url(self, season):
return self.app.get_farmos_url(f"/taxonomy/term/{season.drupal_id}")
def get_xref_buttons(self, season):
buttons = super().get_xref_buttons(season)
if season.farmos_uuid:
buttons.append(
self.make_button(
"View farmOS record",
primary=True,
url=self.request.route_url(
"farmos_seasons.view", uuid=season.farmos_uuid
),
icon_left="eye",
)
)
return buttons
def delete(self):
season = self.get_instance()
if season._plant_assets:
self.request.session.flash(
"Cannot delete season which is still referenced by plant assets.",
"warning",
)
url = self.get_action_url("view", season)
return self.redirect(self.request.get_referrer(default=url))
return super().delete()
def get_row_grid_data(self, season):
model = self.app.model
session = self.Session()
return (
session.query(model.PlantAsset)
.join(model.Asset)
.outerjoin(model.PlantAssetSeason)
.filter(model.PlantAssetSeason.season == season)
)
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)
# 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, plant, i):
return self.request.route_url("plant_assets.view", uuid=plant.uuid)
def ajax_create(self):
"""
AJAX view to create a new season.
"""
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"}
season = model.Season(name=name)
session.add(season)
session.flush()
if self.app.is_farmos_mirror():
client = get_farmos_client_for_user(self.request)
self.app.auto_sync_to_farmos(season, client=client)
return {
"uuid": season.uuid.hex,
"name": season.name,
"farmos_uuid": season.farmos_uuid.hex,
"drupal_id": season.drupal_id,
}
@classmethod
def defaults(cls, config):
""" """
cls._defaults(config)
cls._season_defaults(config)
@classmethod
def _season_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 PlantAssetView(AssetMasterView):
"""
Master view for Plant Assets
"""
model_class = PlantAsset
route_prefix = "plant_assets"
url_prefix = "/assets/plant"
farmos_bundle = "plant"
farmos_refurl_path = "/assets/plant"
labels = {
"plant_types": "Crop/Variety",
"seasons": "Season",
}
grid_columns = [
"thumbnail",
"drupal_id",
"asset_name",
"plant_types",
"season",
"archived",
]
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# plant_types
g.set_renderer("plant_types", self.render_plant_types_for_grid)
def render_plant_types_for_grid(self, plant, field, value):
plant_types = plant._plant_types
if self.farmos_style_grid_links:
links = []
for plant_type in plant_types:
plant_type = plant_type.plant_type
url = self.request.route_url("plant_types.view", uuid=plant_type.uuid)
links.append(tags.link_to(str(plant_type), url))
return ", ".join(links)
return ", ".join([str(pt.plant_type) for pt in plant_types])
def configure_form(self, form):
f = form
super().configure_form(f)
enum = self.app.enum
plant = f.model_instance
# plant_types
f.fields.insert_after("asset_name", "plant_types")
f.set_node("plant_types", PlantTypeRefs(self.request))
if not self.creating:
# nb. must explcitly declare value for non-standard field
f.set_default("plant_types", [pt.uuid for pt in plant.plant_types])
# season
f.fields.insert_after("plant_types", "seasons")
f.set_node("seasons", SeasonRefs(self.request))
f.set_required("seasons", False)
if not self.creating:
# nb. must explcitly declare value for non-standard field
f.set_default("seasons", plant.seasons)
def objectify(self, form):
plant = super().objectify(form)
data = form.validated
self.set_plant_types(plant, data["plant_types"])
self.set_seasons(plant, data["seasons"])
return plant
def set_plant_types(self, plant, desired):
model = self.app.model
session = self.Session()
current = [pt.uuid for pt in plant.plant_types]
for uuid in desired:
if uuid not in current:
plant_type = session.get(model.PlantType, uuid)
assert plant_type
plant.plant_types.append(plant_type)
for uuid in current:
if uuid not in desired:
plant_type = session.get(model.PlantType, uuid)
assert plant_type
plant.plant_types.remove(plant_type)
def set_seasons(self, plant, desired):
model = self.app.model
session = self.Session()
current = [s.uuid for s in plant.seasons]
for uuid in desired:
if uuid not in current:
season = session.get(model.Season, uuid)
assert season
plant.seasons.append(season)
for uuid in current:
if uuid not in desired:
season = session.get(model.Season, uuid)
assert season
plant.seasons.remove(season)
def defaults(config, **kwargs):
base = globals()
PlantTypeView = kwargs.get("PlantTypeView", base["PlantTypeView"])
PlantTypeView.defaults(config)
SeasonView = kwargs.get("SeasonView", base["SeasonView"])
SeasonView.defaults(config)
PlantAssetView = kwargs.get("PlantAssetView", base["PlantAssetView"])
PlantAssetView.defaults(config)
def includeme(config):
defaults(config)