feat: add native table for Groups; import from farmOS API

This commit is contained in:
Lance Edgar 2026-02-13 13:18:52 -06:00
parent 3e5ca3483e
commit 81daa5d913
8 changed files with 393 additions and 1 deletions

View file

@ -0,0 +1,114 @@
"""add Groups
Revision ID: 92b813360b99
Revises: 1b2d3224e5dc
Create Date: 2026-02-13 13:09:48.718064
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "92b813360b99"
down_revision: Union[str, None] = "1b2d3224e5dc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# group
op.create_table(
"group",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("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("active", sa.Boolean(), nullable=False),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True),
sa.Column("drupal_internal_id", sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_group")),
sa.UniqueConstraint(
"drupal_internal_id", name=op.f("uq_group_drupal_internal_id")
),
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_group_farmos_uuid")),
sa.UniqueConstraint("name", name=op.f("uq_group_name")),
)
op.create_table(
"group_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("is_location", sa.Boolean(), autoincrement=False, nullable=True),
sa.Column("is_fixed", sa.Boolean(), autoincrement=False, nullable=True),
sa.Column("active", sa.Boolean(), 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_internal_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_group_version")
),
)
op.create_index(
op.f("ix_group_version_end_transaction_id"),
"group_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_group_version_operation_type"),
"group_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_group_version_pk_transaction_id",
"group_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_group_version_pk_validity",
"group_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_group_version_transaction_id"),
"group_version",
["transaction_id"],
unique=False,
)
def downgrade() -> None:
# group
op.drop_index(op.f("ix_group_version_transaction_id"), table_name="group_version")
op.drop_index("ix_group_version_pk_validity", table_name="group_version")
op.drop_index("ix_group_version_pk_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_end_transaction_id"), table_name="group_version"
)
op.drop_table("group_version")
op.drop_table("group")

View file

@ -34,4 +34,5 @@ from .assets import AssetType
from .land import LandType, LandAsset from .land import LandType, LandAsset
from .structures import StructureType, Structure from .structures import StructureType, Structure
from .animals import AnimalType, Animal from .animals import AnimalType, Animal
from .groups import Group
from .logs import LogType from .logs import LogType

View file

@ -0,0 +1,106 @@
# -*- 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
"""
import sqlalchemy as sa
from sqlalchemy import orm
from wuttjamaican.db import model
class Group(model.Base):
"""
Represents a "group" from farmOS
"""
__tablename__ = "group"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Group",
"model_title_plural": "Groups",
}
uuid = model.uuid_column()
name = sa.Column(
sa.String(length=100),
nullable=False,
unique=True,
doc="""
Name for the group.
""",
)
is_location = sa.Column(
sa.Boolean(),
nullable=False,
doc="""
Whether the group is considered to be a location.
""",
)
is_fixed = sa.Column(
sa.Boolean(),
nullable=False,
doc="""
Whether the group location is fixed.
""",
)
active = sa.Column(
sa.Boolean(),
nullable=False,
doc="""
Whether the group is active.
""",
)
notes = sa.Column(
sa.Text(),
nullable=True,
doc="""
Arbitrary notes for the group.
""",
)
farmos_uuid = sa.Column(
model.UUID(),
nullable=True,
unique=True,
doc="""
UUID for the group within farmOS.
""",
)
drupal_internal_id = sa.Column(
sa.Integer(),
nullable=True,
unique=True,
doc="""
Drupal internal ID for the group.
""",
)
def __str__(self):
return self.name or ""

View file

@ -101,6 +101,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
importers["Structure"] = StructureImporter importers["Structure"] = StructureImporter
importers["AnimalType"] = AnimalTypeImporter importers["AnimalType"] = AnimalTypeImporter
importers["Animal"] = AnimalImporter importers["Animal"] = AnimalImporter
importers["Group"] = GroupImporter
importers["LogType"] = LogTypeImporter importers["LogType"] = LogTypeImporter
return importers return importers
@ -279,6 +280,44 @@ class AssetTypeImporter(FromFarmOS, ToWutta):
} }
class GroupImporter(FromFarmOS, ToWutta):
"""
farmOS API WuttaFarm importer for Groups
"""
model_class = model.Group
supported_fields = [
"farmos_uuid",
"drupal_internal_id",
"name",
"is_location",
"is_fixed",
"notes",
"active",
]
def get_source_objects(self):
""" """
groups = self.farmos_client.asset.get("group")
return groups["data"]
def normalize_source_object(self, group):
""" """
if notes := group["attributes"]["notes"]:
notes = notes["value"]
return {
"farmos_uuid": UUID(group["id"]),
"drupal_internal_id": group["attributes"]["drupal_internal__id"],
"name": group["attributes"]["name"],
"is_location": group["attributes"]["is_location"],
"is_fixed": group["attributes"]["is_fixed"],
"active": group["attributes"]["status"] == "active",
"notes": notes,
}
class LandAssetImporter(FromFarmOS, ToWutta): class LandAssetImporter(FromFarmOS, ToWutta):
""" """
farmOS API WuttaFarm importer for Land Assets farmOS API WuttaFarm importer for Land Assets

View file

@ -49,6 +49,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "animals", "route": "animals",
"perm": "animals.list", "perm": "animals.list",
}, },
{
"title": "Groups",
"route": "groups",
"perm": "groups.list",
},
{ {
"title": "Structures", "title": "Structures",
"route": "structures", "route": "structures",

View file

@ -48,6 +48,7 @@ def includeme(config):
config.include("wuttafarm.web.views.land_assets") config.include("wuttafarm.web.views.land_assets")
config.include("wuttafarm.web.views.structures") config.include("wuttafarm.web.views.structures")
config.include("wuttafarm.web.views.animals") config.include("wuttafarm.web.views.animals")
config.include("wuttafarm.web.views.groups")
config.include("wuttafarm.web.views.log_types") config.include("wuttafarm.web.views.log_types")
# views for farmOS # views for farmOS

View file

@ -141,7 +141,10 @@ class GroupView(FarmOSMasterView):
f.set_widget("changed", WuttaDateTimeWidget(self.request)) f.set_widget("changed", WuttaDateTimeWidget(self.request))
def get_xref_buttons(self, group): def get_xref_buttons(self, group):
return [ model = self.app.model
session = self.Session()
buttons = [
self.make_button( self.make_button(
"View in farmOS", "View in farmOS",
primary=True, primary=True,
@ -151,6 +154,22 @@ class GroupView(FarmOSMasterView):
), ),
] ]
if wf_group := (
session.query(model.Group)
.filter(model.Group.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),
icon_left="eye",
)
)
return buttons
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -0,0 +1,107 @@
# -*- 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 Groups
"""
from wuttafarm.db.model.groups import Group
from wuttafarm.web.views import WuttaFarmMasterView
class GroupView(WuttaFarmMasterView):
"""
Master view for Groups
"""
model_class = Group
route_prefix = "groups"
url_prefix = "/groups"
farmos_refurl_path = "/assets/group"
grid_columns = [
"name",
"is_location",
"is_fixed",
"active",
]
sort_defaults = "name"
filter_defaults = {
"name": {"active": True, "verb": "contains"},
}
form_fields = [
"name",
"is_location",
"is_fixed",
"active",
"notes",
"farmos_uuid",
"drupal_internal_id",
]
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_internal_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()
GroupView = kwargs.get("GroupView", base["GroupView"])
GroupView.defaults(config)
def includeme(config):
defaults(config)