feat: add support for log 'owners'
This commit is contained in:
parent
ae73d2f87f
commit
64e4392a92
6 changed files with 259 additions and 17 deletions
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")
|
||||||
|
|
@ -154,7 +154,12 @@ class Log(model.Base):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
_assets = orm.relationship("LogAsset", back_populates="log")
|
_assets = orm.relationship(
|
||||||
|
"LogAsset",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
cascade_backrefs=False,
|
||||||
|
back_populates="log",
|
||||||
|
)
|
||||||
|
|
||||||
assets = association_proxy(
|
assets = association_proxy(
|
||||||
"_assets",
|
"_assets",
|
||||||
|
|
@ -162,6 +167,19 @@ class Log(model.Base):
|
||||||
creator=lambda asset: LogAsset(asset=asset),
|
creator=lambda asset: LogAsset(asset=asset),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_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):
|
def __str__(self):
|
||||||
return self.message or ""
|
return self.message or ""
|
||||||
|
|
||||||
|
|
@ -192,6 +210,7 @@ def add_log_proxies(subclass):
|
||||||
Log.make_proxy(subclass, "log", "status")
|
Log.make_proxy(subclass, "log", "status")
|
||||||
Log.make_proxy(subclass, "log", "notes")
|
Log.make_proxy(subclass, "log", "notes")
|
||||||
Log.make_proxy(subclass, "log", "assets")
|
Log.make_proxy(subclass, "log", "assets")
|
||||||
|
Log.make_proxy(subclass, "log", "owners")
|
||||||
|
|
||||||
|
|
||||||
class LogAsset(model.Base):
|
class LogAsset(model.Base):
|
||||||
|
|
@ -216,3 +235,27 @@ class LogAsset(model.Base):
|
||||||
"Asset",
|
"Asset",
|
||||||
foreign_keys=asset_uuid,
|
foreign_keys=asset_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -979,6 +979,7 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
fields.extend(
|
fields.extend(
|
||||||
[
|
[
|
||||||
"assets",
|
"assets",
|
||||||
|
"owners",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
@ -1004,6 +1005,9 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
(a["asset_type"], UUID(a["uuid"])) for a in data["assets"]
|
(a["asset_type"], UUID(a["uuid"])) for a in data["assets"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if "owners" in self.fields:
|
||||||
|
data["owners"] = [UUID(uuid) for uuid in data["owner_uuids"]]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def normalize_target_object(self, log):
|
def normalize_target_object(self, log):
|
||||||
|
|
@ -1011,9 +1015,12 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
|
|
||||||
if "assets" in self.fields:
|
if "assets" in self.fields:
|
||||||
data["assets"] = [
|
data["assets"] = [
|
||||||
(a.asset.asset_type, a.asset.farmos_uuid) for a in log.log._assets
|
(asset.asset_type, asset.farmos_uuid) for asset in log.assets
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if "owners" in self.fields:
|
||||||
|
data["owners"] = [user.farmos_uuid for user in log.owners]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def update_target_object(self, log, source_data, target_data=None):
|
def update_target_object(self, log, source_data, target_data=None):
|
||||||
|
|
@ -1026,14 +1033,13 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
for key in source_data["assets"]:
|
for key in source_data["assets"]:
|
||||||
asset_type, farmos_uuid = key
|
asset_type, farmos_uuid = key
|
||||||
if not target_data or key not in target_data["assets"]:
|
if not target_data or key not in target_data["assets"]:
|
||||||
self.target_session.flush()
|
|
||||||
asset = (
|
asset = (
|
||||||
self.target_session.query(model.Asset)
|
self.target_session.query(model.Asset)
|
||||||
.filter(model.Asset.asset_type == asset_type)
|
.filter(model.Asset.asset_type == asset_type)
|
||||||
.filter(model.Asset.farmos_uuid == farmos_uuid)
|
.filter(model.Asset.farmos_uuid == farmos_uuid)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
log.log._assets.append(model.LogAsset(asset=asset))
|
log.assets.append(asset)
|
||||||
|
|
||||||
if target_data:
|
if target_data:
|
||||||
for key in target_data["assets"]:
|
for key in target_data["assets"]:
|
||||||
|
|
@ -1045,13 +1051,31 @@ class LogImporterBase(FromFarmOS, ToWutta):
|
||||||
.filter(model.Asset.farmos_uuid == farmos_uuid)
|
.filter(model.Asset.farmos_uuid == farmos_uuid)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
asset = (
|
log.assets.remove(asset)
|
||||||
self.target_session.query(model.LogAsset)
|
|
||||||
.filter(model.LogAsset.log == log)
|
if "owners" in self.fields:
|
||||||
.filter(model.LogAsset.asset == asset)
|
if not target_data or target_data["owners"] != source_data["owners"]:
|
||||||
|
|
||||||
|
for farmos_uuid in source_data["owners"]:
|
||||||
|
if not target_data or farmos_uuid not in target_data["assets"]:
|
||||||
|
user = (
|
||||||
|
self.target_session.query(model.User)
|
||||||
|
.join(model.WuttaFarmUser)
|
||||||
|
.filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
self.target_session.delete(asset)
|
log.owners.append(user)
|
||||||
|
|
||||||
|
if target_data:
|
||||||
|
for farmos_uuid in target_data["owners"]:
|
||||||
|
if farmos_uuid not in source_data["owners"]:
|
||||||
|
user = (
|
||||||
|
self.target_session.query(model.User)
|
||||||
|
.join(model.WuttaFarmUser)
|
||||||
|
.filter(model.WuttaFarmUser.farmos_uuid == farmos_uuid)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
log.owners.remove(user)
|
||||||
|
|
||||||
return log
|
return log
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -371,9 +371,9 @@ class LogAssetRefs(WuttaSet):
|
||||||
|
|
||||||
def serialize(self, node, appstruct):
|
def serialize(self, node, appstruct):
|
||||||
if not appstruct:
|
if not appstruct:
|
||||||
appstruct = []
|
return colander.null
|
||||||
uuids = [u.hex for u in appstruct]
|
|
||||||
return json.dumps(uuids)
|
return {asset.uuid for asset in appstruct}
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
def widget_maker(self, **kwargs):
|
||||||
from wuttafarm.web.forms.widgets import LogAssetRefsWidget
|
from wuttafarm.web.forms.widgets import LogAssetRefsWidget
|
||||||
|
|
@ -381,6 +381,23 @@ class LogAssetRefs(WuttaSet):
|
||||||
return LogAssetRefsWidget(self.request, **kwargs)
|
return LogAssetRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LogOwnerRefs(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 LogOwnerRefsWidget
|
||||||
|
|
||||||
|
return LogOwnerRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Notes(colander.String):
|
class Notes(colander.String):
|
||||||
"""
|
"""
|
||||||
Custom schema type for "note" fields.
|
Custom schema type for "note" fields.
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,7 @@ class LogAssetRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
readonly = kw.get("readonly", self.readonly)
|
readonly = kw.get("readonly", self.readonly)
|
||||||
if readonly:
|
if readonly:
|
||||||
assets = []
|
assets = []
|
||||||
for uuid in json.loads(cstruct):
|
for uuid in cstruct or []:
|
||||||
asset = session.get(model.Asset, uuid)
|
asset = session.get(model.Asset, uuid)
|
||||||
assets.append(
|
assets.append(
|
||||||
HTML.tag(
|
HTML.tag(
|
||||||
|
|
@ -454,6 +454,37 @@ class LogAssetRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class LogOwnerRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
"""
|
||||||
|
Widget for Owners 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:
|
||||||
|
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):
|
class AnimalTypeRefWidget(ObjectRefWidget):
|
||||||
"""
|
"""
|
||||||
Custom widget which uses the ``<animal-type-picker>`` component.
|
Custom widget which uses the ``<animal-type-picker>`` component.
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ from wuttaweb.forms.widgets import WuttaDateTimeWidget
|
||||||
|
|
||||||
from wuttafarm.web.views import WuttaFarmMasterView
|
from wuttafarm.web.views import WuttaFarmMasterView
|
||||||
from wuttafarm.db.model import LogType, Log
|
from wuttafarm.db.model import LogType, Log
|
||||||
from wuttafarm.web.forms.schema import LogAssetRefs
|
from wuttafarm.web.forms.schema import LogAssetRefs, LogOwnerRefs
|
||||||
from wuttafarm.util import get_log_type_enum
|
from wuttafarm.util import get_log_type_enum
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -99,7 +99,6 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"message": "Log Name",
|
"message": "Log Name",
|
||||||
"owners": "Owner",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
|
@ -181,6 +180,10 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
# assets
|
# assets
|
||||||
g.set_renderer("assets", self.render_assets_for_grid)
|
g.set_renderer("assets", self.render_assets_for_grid)
|
||||||
|
|
||||||
|
# owners
|
||||||
|
g.set_label("owners", "Owner")
|
||||||
|
g.set_renderer("owners", self.render_owners_for_grid)
|
||||||
|
|
||||||
def render_assets_for_grid(self, log, field, value):
|
def render_assets_for_grid(self, log, field, value):
|
||||||
|
|
||||||
if self.farmos_style_grid_links:
|
if self.farmos_style_grid_links:
|
||||||
|
|
@ -194,6 +197,17 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
|
|
||||||
return ", ".join([str(a) for a in log.assets])
|
return ", ".join([str(a) for a in log.assets])
|
||||||
|
|
||||||
|
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):
|
def grid_row_class(self, log, data, i):
|
||||||
if log.status == "pending":
|
if log.status == "pending":
|
||||||
return "has-background-warning"
|
return "has-background-warning"
|
||||||
|
|
@ -219,7 +233,8 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
f.remove("assets") # TODO: need to support this
|
f.remove("assets") # TODO: need to support this
|
||||||
else:
|
else:
|
||||||
f.set_node("assets", LogAssetRefs(self.request))
|
f.set_node("assets", LogAssetRefs(self.request))
|
||||||
f.set_default("assets", [a.asset_uuid for a in log.log._assets])
|
# nb. must explicity declare value for non-standard field
|
||||||
|
f.set_default("assets", log.assets)
|
||||||
|
|
||||||
# location
|
# location
|
||||||
if self.creating or self.editing:
|
if self.creating or self.editing:
|
||||||
|
|
@ -247,6 +262,10 @@ class LogMasterView(WuttaFarmMasterView):
|
||||||
# owners
|
# owners
|
||||||
if self.creating or self.editing:
|
if self.creating or self.editing:
|
||||||
f.remove("owners") # TODO: need to support this
|
f.remove("owners") # TODO: need to support this
|
||||||
|
else:
|
||||||
|
f.set_node("owners", LogOwnerRefs(self.request))
|
||||||
|
# nb. must explicity declare value for non-standard field
|
||||||
|
f.set_default("owners", log.owners)
|
||||||
|
|
||||||
# status
|
# status
|
||||||
f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS))
|
f.set_node("status", WuttaDictEnum(self.request, enum.LOG_STATUS))
|
||||||
|
|
@ -331,7 +350,7 @@ class AllLogView(LogMasterView):
|
||||||
"quantity",
|
"quantity",
|
||||||
"groups",
|
"groups",
|
||||||
"is_group_assignment",
|
"is_group_assignment",
|
||||||
"owner",
|
"owners",
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, grid):
|
def configure_grid(self, grid):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue