diff --git a/src/wuttafarm/db/alembic/versions/6c56bcd1c028_add_wuttafarmuser.py b/src/wuttafarm/db/alembic/versions/6c56bcd1c028_add_wuttafarmuser.py new file mode 100644 index 0000000..32fa175 --- /dev/null +++ b/src/wuttafarm/db/alembic/versions/6c56bcd1c028_add_wuttafarmuser.py @@ -0,0 +1,114 @@ +"""add WuttaFarmUser + +Revision ID: 6c56bcd1c028 +Revises: 2b6385d0fa17 +Create Date: 2026-02-09 20:46:20.995903 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import wuttjamaican.db.util + + +# revision identifiers, used by Alembic. +revision: str = "6c56bcd1c028" +down_revision: Union[str, None] = "2b6385d0fa17" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # wuttafarm_user + op.create_table( + "wuttafarm_user", + sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False), + sa.Column("farmos_uuid", wuttjamaican.db.util.UUID(), nullable=True), + sa.Column("drupal_internal_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["uuid"], ["user.uuid"], name=op.f("fk_wuttafarm_user_uuid_user") + ), + sa.PrimaryKeyConstraint("uuid", name=op.f("pk_wuttafarm_user")), + ) + op.create_table( + "wuttafarm_user_version", + sa.Column( + "uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False + ), + 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_wuttafarm_user_version") + ), + ) + op.create_index( + op.f("ix_wuttafarm_user_version_end_transaction_id"), + "wuttafarm_user_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_wuttafarm_user_version_operation_type"), + "wuttafarm_user_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_wuttafarm_user_version_pk_transaction_id", + "wuttafarm_user_version", + ["uuid", sa.literal_column("transaction_id DESC")], + unique=False, + ) + op.create_index( + "ix_wuttafarm_user_version_pk_validity", + "wuttafarm_user_version", + ["uuid", "transaction_id", "end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_wuttafarm_user_version_transaction_id"), + "wuttafarm_user_version", + ["transaction_id"], + unique=False, + ) + + +def downgrade() -> None: + + # wuttafarm_user + op.drop_index( + op.f("ix_wuttafarm_user_version_transaction_id"), + table_name="wuttafarm_user_version", + ) + op.drop_index( + "ix_wuttafarm_user_version_pk_validity", table_name="wuttafarm_user_version" + ) + op.drop_index( + "ix_wuttafarm_user_version_pk_transaction_id", + table_name="wuttafarm_user_version", + ) + op.drop_index( + op.f("ix_wuttafarm_user_version_operation_type"), + table_name="wuttafarm_user_version", + ) + op.drop_index( + op.f("ix_wuttafarm_user_version_end_transaction_id"), + table_name="wuttafarm_user_version", + ) + op.drop_table("wuttafarm_user_version") + op.drop_table("wuttafarm_user") diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index d0693cb..29e4dc0 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -26,5 +26,8 @@ WuttaFarm data models # bring in all of wutta from wuttjamaican.db.model import * -# wuttafarm models +# wutta model extensions +from .users import WuttaFarmUser + +# wuttafarm proper models from .animals import AnimalType diff --git a/src/wuttafarm/db/model/users.py b/src/wuttafarm/db/model/users.py new file mode 100644 index 0000000..2cad429 --- /dev/null +++ b/src/wuttafarm/db/model/users.py @@ -0,0 +1,80 @@ +# -*- 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 . +# +################################################################################ +""" +Model definition for Users (extension) +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from wuttjamaican.db import model + + +class WuttaFarmUser(model.Base): + """ + WuttaFarm extension for the User model. + """ + + __tablename__ = "wuttafarm_user" + __versioned__ = {} + + uuid = model.uuid_column(sa.ForeignKey("user.uuid"), default=None) + + user = orm.relationship( + model.User, + doc=""" + Reference to the User which this record extends. + """, + backref=orm.backref( + "_wuttafarm", + uselist=False, + cascade="all, delete-orphan", + cascade_backrefs=False, + doc=""" + Reference to the WuttaFarm-specific extension record for + the user. + """, + ), + ) + + farmos_uuid = sa.Column( + model.UUID(), + nullable=True, + doc=""" + UUID for the user within farmOS + """, + ) + + drupal_internal_id = sa.Column( + sa.Integer(), + nullable=True, + doc=""" + Drupal internal ID for the user. + """, + ) + + def __str__(self): + return str(self.user or "") + + +WuttaFarmUser.make_proxy(model.User, "_wuttafarm", "farmos_uuid") +WuttaFarmUser.make_proxy(model.User, "_wuttafarm", "drupal_internal_id") diff --git a/src/wuttafarm/importing/farmos.py b/src/wuttafarm/importing/farmos.py index e9e4735..3c6eea9 100644 --- a/src/wuttafarm/importing/farmos.py +++ b/src/wuttafarm/importing/farmos.py @@ -89,6 +89,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler): def define_importers(self): """ """ importers = super().define_importers() + importers["User"] = UserImporter importers["AnimalType"] = AnimalTypeImporter return importers @@ -152,3 +153,56 @@ class AnimalTypeImporter(FromFarmOS, ToWutta): "description": animal_type["attributes"]["description"], "changed": self.normalize_datetime(animal_type["attributes"]["changed"]), } + + +class UserImporter(FromFarmOS, ToWutta): + """ + farmOS API → WuttaFarm importer for Users + """ + + model_class = model.User + + supported_fields = [ + "farmos_uuid", + "drupal_internal_id", + "username", + ] + + def get_simple_fields(self): + """ """ + fields = list(super().get_simple_fields()) + # nb. must explicitly declare extension fields + fields.extend( + [ + "farmos_uuid", + "drupal_internal_id", + ] + ) + return fields + + def get_source_objects(self): + """ """ + users = self.farmos_client.resource.get("user") + return users["data"] + + def normalize_source_object(self, user): + """ """ + + # nb. skip Anonymous user which does not have drupal id + drupal_internal_id = user["attributes"].get("drupal_internal__uid") + if not drupal_internal_id: + return None + + return { + "farmos_uuid": UUID(user["id"]), + "drupal_internal_id": drupal_internal_id, + "username": user["attributes"]["name"], + } + + def can_delete_object(self, user, data=None): + """ + Prevent delete for users which do not exist in farmOS. + """ + if not user.farmos_uuid: + return False + return True diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index 86dcd81..25a4054 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -36,6 +36,7 @@ def includeme(config): **{ "wuttaweb.views.auth": "wuttafarm.web.views.auth", "wuttaweb.views.common": "wuttafarm.web.views.common", + "wuttaweb.views.users": "wuttafarm.web.views.users", } ) diff --git a/src/wuttafarm/web/views/farmos/users.py b/src/wuttafarm/web/views/farmos/users.py index fa47d34..bb93066 100644 --- a/src/wuttafarm/web/views/farmos/users.py +++ b/src/wuttafarm/web/views/farmos/users.py @@ -116,17 +116,36 @@ class UserView(FarmOSMasterView): f.set_node("changed", WuttaDateTime()) def get_xref_buttons(self, user): + model = self.app.model + session = self.Session() + buttons = [] + if drupal_id := user["drupal_internal_id"]: - return [ + buttons.append( self.make_button( "View in farmOS", primary=True, url=self.app.get_farmos_url(f"/user/{drupal_id}"), target="_blank", icon_left="external-link-alt", - ), - ] - return None + ) + ) + + if wf_user := ( + session.query(model.WuttaFarmUser) + .filter(model.WuttaFarmUser.farmos_uuid == user["uuid"]) + .first() + ): + buttons.append( + self.make_button( + f"View {self.app.get_title()} record", + primary=True, + url=self.request.route_url("users.view", uuid=wf_user.uuid), + icon_left="eye", + ) + ) + + return buttons def defaults(config, **kwargs): diff --git a/src/wuttafarm/web/views/users.py b/src/wuttafarm/web/views/users.py new file mode 100644 index 0000000..782ab16 --- /dev/null +++ b/src/wuttafarm/web/views/users.py @@ -0,0 +1,97 @@ +# -*- 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 . +# +################################################################################ +""" +Views for Users +""" + +from wuttaweb.views import users as base + + +class UserView(base.UserView): + """ + Custom master view for Users. + """ + + labels = { + "farmos_uuid": "farmOS UUID", + "drupal_internal_id": "Drupal Internal ID", + } + + def get_template_context(self, context): + context = super().get_template_context(context) + + if self.listing: + context["farmos_refurl"] = self.app.get_farmos_url("/admin/people") + + return context + + def configure_form(self, form): + """ """ + f = form + super().configure_form(f) + user = f.model_instance + + # farmos_uuid + f.fields.append("farmos_uuid") + f.set_default("farmos_uuid", user.farmos_uuid) + + # drupal_internal_id + f.fields.append("drupal_internal_id") + f.set_default("drupal_internal_id", user.drupal_internal_id) + + def get_xref_buttons(self, user): + buttons = [] + + if user.drupal_internal_id: + buttons.append( + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url(f"/user/{user.drupal_internal_id}"), + target="_blank", + icon_left="external-link-alt", + ) + ) + + if user.farmos_uuid: + buttons.append( + self.make_button( + "View farmOS record", + primary=True, + url=self.request.route_url( + "farmos_users.view", uuid=user.farmos_uuid + ), + icon_left="eye", + ) + ) + + return buttons + + +def defaults(config, **kwargs): + local = globals() + UserView = kwargs.get("UserView", local["UserView"]) + base.defaults(config, **{"UserView": UserView}) + + +def includeme(config): + defaults(config)