feat: add native table for Activity Logs; import from farmOS API

This commit is contained in:
Lance Edgar 2026-02-13 14:50:52 -06:00
parent f4e4c3efb3
commit ed768a83d0
8 changed files with 357 additions and 2 deletions

View file

@ -0,0 +1,118 @@
"""add Activity Logs
Revision ID: 3e2ef02bf264
Revises: 92b813360b99
Create Date: 2026-02-13 14:36:47.191922
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "3e2ef02bf264"
down_revision: Union[str, None] = "92b813360b99"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# log_activity
op.create_table(
"log_activity",
sa.Column("uuid", wuttjamaican.db.util.UUID(), 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.PrimaryKeyConstraint("uuid", name=op.f("pk_log_activity")),
sa.UniqueConstraint("drupal_id", name=op.f("uq_log_activity_drupal_id")),
sa.UniqueConstraint("farmos_uuid", name=op.f("uq_log_activity_farmos_uuid")),
)
op.create_table(
"log_activity_version",
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
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_activity_version")
),
)
op.create_index(
op.f("ix_log_activity_version_end_transaction_id"),
"log_activity_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_log_activity_version_operation_type"),
"log_activity_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_log_activity_version_pk_transaction_id",
"log_activity_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_log_activity_version_pk_validity",
"log_activity_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_log_activity_version_transaction_id"),
"log_activity_version",
["transaction_id"],
unique=False,
)
def downgrade() -> None:
# log_activity
op.drop_index(
op.f("ix_log_activity_version_transaction_id"),
table_name="log_activity_version",
)
op.drop_index(
"ix_log_activity_version_pk_validity", table_name="log_activity_version"
)
op.drop_index(
"ix_log_activity_version_pk_transaction_id", table_name="log_activity_version"
)
op.drop_index(
op.f("ix_log_activity_version_operation_type"),
table_name="log_activity_version",
)
op.drop_index(
op.f("ix_log_activity_version_end_transaction_id"),
table_name="log_activity_version",
)
op.drop_table("log_activity_version")
op.drop_table("log_activity")

View file

@ -35,4 +35,4 @@ 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 .groups import Group
from .logs import LogType from .logs import LogType, ActivityLog

View file

@ -80,3 +80,71 @@ class LogType(model.Base):
def __str__(self): def __str__(self):
return self.name or "" 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.
""",
)
drupal_id = sa.Column(
sa.Integer(),
nullable=True,
unique=True,
doc="""
Drupal internal ID for the log.
""",
)
def __str__(self):
return self.message or ""

View file

@ -103,6 +103,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
importers["Animal"] = AnimalImporter importers["Animal"] = AnimalImporter
importers["Group"] = GroupImporter importers["Group"] = GroupImporter
importers["LogType"] = LogTypeImporter importers["LogType"] = LogTypeImporter
importers["ActivityLog"] = ActivityLogImporter
return importers return importers
@ -136,6 +137,43 @@ class FromFarmOS(Importer):
return self.app.make_utc(dt) return self.app.make_utc(dt)
class ActivityLogImporter(FromFarmOS, ToWutta):
"""
farmOS API WuttaFarm importer for Activity Logs
"""
model_class = model.ActivityLog
supported_fields = [
"farmos_uuid",
"drupal_id",
"message",
"timestamp",
"notes",
"status",
]
def get_source_objects(self):
""" """
logs = self.farmos_client.log.get("activity")
return logs["data"]
def normalize_source_object(self, log):
""" """
if notes := log["attributes"]["notes"]:
notes = notes["value"]
return {
"farmos_uuid": UUID(log["id"]),
"drupal_id": log["attributes"]["drupal_internal__id"],
"message": log["attributes"]["name"],
"timestamp": self.normalize_datetime(log["attributes"]["timestamp"]),
"notes": notes,
"status": log["attributes"]["status"],
}
class AnimalImporter(FromFarmOS, ToWutta): class AnimalImporter(FromFarmOS, ToWutta):
""" """
farmOS API WuttaFarm importer for Animals farmOS API WuttaFarm importer for Animals

View file

@ -93,6 +93,12 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"title": "Logs", "title": "Logs",
"type": "menu", "type": "menu",
"items": [ "items": [
{
"title": "Activity Logs",
"route": "activity_logs",
"perm": "activity_logs.list",
},
{"type": "sep"},
{ {
"title": "Log Types", "title": "Log Types",
"route": "log_types", "route": "log_types",

View file

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

View file

@ -115,7 +115,10 @@ class ActivityLogView(FarmOSMasterView):
f.set_widget("notes", "notes") f.set_widget("notes", "notes")
def get_xref_buttons(self, log): def get_xref_buttons(self, log):
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,
@ -125,6 +128,22 @@ class ActivityLogView(FarmOSMasterView):
), ),
] ]
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): def defaults(config, **kwargs):
base = globals() base = globals()

View file

@ -0,0 +1,105 @@
# -*- 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 Activity Logs
"""
from wuttafarm.db.model.logs import ActivityLog
from wuttafarm.web.views import WuttaFarmMasterView
class ActivityLogView(WuttaFarmMasterView):
"""
Master view for Activity Logs
"""
model_class = ActivityLog
route_prefix = "activity_logs"
url_prefix = "/logs/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()
ActivityLogView = kwargs.get("ActivityLogView", base["ActivityLogView"])
ActivityLogView.defaults(config)
def includeme(config):
defaults(config)