From e0f91417cb209ee21dfde497a938cc2041e0f321 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 3 Feb 2026 19:34:35 -0600 Subject: [PATCH] feat: initial basic app to prove display of API (animal) data --- pyproject.toml | 7 +- src/wuttafarm/__init__.py | 26 ++++ src/wuttafarm/_version.py | 9 ++ src/wuttafarm/app.py | 57 +++++++ src/wuttafarm/auth.py | 117 ++++++++++++++ src/wuttafarm/cli.py | 35 ++++- src/wuttafarm/config.py | 42 ++++- src/wuttafarm/db/model/__init__.py | 20 +++ src/wuttafarm/farmos.py | 57 +++++++ src/wuttafarm/web/app.py | 20 +++ src/wuttafarm/web/forms/__init__.py | 0 src/wuttafarm/web/forms/widgets.py | 47 ++++++ src/wuttafarm/web/menus.py | 42 +++-- src/wuttafarm/web/static/__init__.py | 20 +++ src/wuttafarm/web/subscribers.py | 26 +++- src/wuttafarm/web/templates/base.mako | 16 ++ src/wuttafarm/web/views/__init__.py | 36 ++++- src/wuttafarm/web/views/auth.py | 58 +++++++ src/wuttafarm/web/views/common.py | 65 ++++++++ src/wuttafarm/web/views/farmos/__init__.py | 30 ++++ src/wuttafarm/web/views/farmos/animals.py | 171 +++++++++++++++++++++ src/wuttafarm/web/views/farmos/master.py | 65 ++++++++ tasks.py | 24 +++ 23 files changed, 950 insertions(+), 40 deletions(-) create mode 100644 src/wuttafarm/__init__.py create mode 100644 src/wuttafarm/_version.py create mode 100644 src/wuttafarm/app.py create mode 100644 src/wuttafarm/auth.py create mode 100644 src/wuttafarm/farmos.py create mode 100644 src/wuttafarm/web/forms/__init__.py create mode 100644 src/wuttafarm/web/forms/widgets.py create mode 100644 src/wuttafarm/web/templates/base.mako create mode 100644 src/wuttafarm/web/views/auth.py create mode 100644 src/wuttafarm/web/views/common.py create mode 100644 src/wuttafarm/web/views/farmos/__init__.py create mode 100644 src/wuttafarm/web/views/farmos/animals.py create mode 100644 src/wuttafarm/web/views/farmos/master.py create mode 100644 tasks.py diff --git a/pyproject.toml b/pyproject.toml index 837096d..cea7dcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaFarm" version = "0.0.0" -description = "Web app to expose / extend farmOS" +description = "Web app to integrate with and extend farmOS" readme = "README.md" authors = [ {name = "Lance Edgar", email = "lance@wuttaproject.org"} @@ -29,8 +29,9 @@ classifiers = [ ] license = {text = "GNU General Public License v3"} dependencies = [ - "psycopg2", - "WuttaWeb[continuum]", + "farmOS", + "psycopg2", + "WuttaWeb[continuum]", ] diff --git a/src/wuttafarm/__init__.py b/src/wuttafarm/__init__.py new file mode 100644 index 0000000..a48e2e6 --- /dev/null +++ b/src/wuttafarm/__init__.py @@ -0,0 +1,26 @@ +# -*- 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 . +# +################################################################################ +""" +package root +""" + +from ._version import __version__ diff --git a/src/wuttafarm/_version.py b/src/wuttafarm/_version.py new file mode 100644 index 0000000..bbd4c45 --- /dev/null +++ b/src/wuttafarm/_version.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8; -*- +""" +Package Version +""" + +from importlib.metadata import version + + +__version__ = version("WuttaFarm") diff --git a/src/wuttafarm/app.py b/src/wuttafarm/app.py new file mode 100644 index 0000000..bafbdad --- /dev/null +++ b/src/wuttafarm/app.py @@ -0,0 +1,57 @@ +# -*- 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 . +# +################################################################################ +""" +Custom app handler for WuttaFarm +""" + +from wuttjamaican import app as base + + +class WuttaFarmAppHandler(base.AppHandler): + """ + Custom :term:`app handler` for WuttaFarm. + """ + + default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler" + + def get_farmos_handler(self): + """ + Get the configured farmOS integration handler. + + :rtype: :class:`~wuttafarm.farmos.FarmOSHandler` + """ + if "farmos" not in self.handlers: + spec = self.config.get( + f"{self.appname}.farmos_handler", + default="wuttafarm.farmos:FarmOSHandler", + ) + factory = self.load_object(spec) + self.handlers["farmos"] = factory(self.config) + return self.handlers["farmos"] + + def get_farmos_url(self, *args, **kwargs): + """ + Get a farmOS URL. This is a convenience wrapper around + :meth:`~wuttafarm.farmos.FarmOSHandler.get_farmos_url()`. + """ + handler = self.get_farmos_handler() + return handler.get_farmos_url(*args, **kwargs) diff --git a/src/wuttafarm/auth.py b/src/wuttafarm/auth.py new file mode 100644 index 0000000..ab3f67a --- /dev/null +++ b/src/wuttafarm/auth.py @@ -0,0 +1,117 @@ +# -*- 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 . +# +################################################################################ +""" +Auth handler for use with farmOS +""" + +from uuid import UUID + +from farmOS import farmOS +from oauthlib.oauth2.rfc6749.errors import InvalidGrantError +from sqlalchemy import orm + +from wuttjamaican.auth import AuthHandler + + +class WuttaFarmAuthHandler(AuthHandler): + """ + Custom auth handler for WuttaFarm. This adds some magic around + the user login process. + + It can attempt authentication against the configured farmOS + instance, auto-creating native users within the app DB as needed. + + """ + + def get_role_farm_manager(self, session): + """ + Returns the special "Farm Manager" role. + """ + return self._special_role( + session, UUID("06979646-b1b9-723b-8000-b86161427f9f"), "Farm Manager" + ) + + def get_role_farm_worker(self, session): + """ + Returns the special "Farm Worker" role. + """ + return self._special_role( + session, UUID("06979648-9bd6-7b61-8000-ee8818052ee8"), "Farm Worker" + ) + + def get_role_farm_viewer(self, session): + """ + Returns the special "Farm Viewer" role. + """ + return self._special_role( + session, UUID("06979649-ed32-7e97-8000-113ddf0ab5f3"), "Farm Viewer" + ) + + def authenticate_user(self, session, username, password): + """ + When authentication is attempted, this first will check + credentials against the app DB per normal logic. If that + succeeds, the result is no different from the typical + behavior. + + If default logic fails, this will try to obtain an OAuth2 + token from the farmOS site. If that succeeds, then a lookup + is done in the app DB for a matching user. If the user does + not yet exist it will be created automatically. + """ + if user := super().authenticate_user(session, username, password): + return user + + if token := self.get_farmos_oauth2_token(username, password): + if user := self.get_or_make_farmos_user(session, username): + user.__dict__["farmos_oauth2_token"] = token + return user + + return None + + def get_farmos_oauth2_token(self, username, password): + url = self.app.get_farmos_url() + client = farmOS(url) + try: + return client.authorize(username=username, password=password) + except InvalidGrantError: + return None + + def get_or_make_farmos_user(self, session, username): + model = self.app.model + try: + user = ( + session.query(model.User).filter(model.User.username == username).one() + ) + except orm.exc.NoResultFound: + pass + else: + return user if user.active else None + + # nb. prevent edit for farmOS mirrored user accounts + user = self.make_user(session, username=username, prevent_edit=True) + + # TODO + manager = self.get_role_farm_manager(session) + user.roles.append(manager) + + return user diff --git a/src/wuttafarm/cli.py b/src/wuttafarm/cli.py index 702d572..2f377a3 100644 --- a/src/wuttafarm/cli.py +++ b/src/wuttafarm/cli.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ WuttaFarm CLI """ @@ -9,22 +29,23 @@ from wuttjamaican.cli import make_typer wuttafarm_typer = make_typer( - name='wuttafarm', - help="WuttaFarm -- Web app to expose / extend farmOS" + name="wuttafarm", help="WuttaFarm -- Web app to integrate with and extend farmOS" ) @wuttafarm_typer.command() def install( - ctx: typer.Context, + ctx: typer.Context, ): """ Install the WuttaFarm app """ config = ctx.parent.wutta_config app = config.get_app() - install = app.get_install_handler(pkg_name='wuttafarm', - app_title="WuttaFarm", - pypi_name='WuttaFarm', - egg_name='WuttaFarm') + install = app.get_install_handler( + pkg_name="wuttafarm", + app_title="WuttaFarm", + pypi_name="WuttaFarm", + egg_name="WuttaFarm", + ) install.run() diff --git a/src/wuttafarm/config.py b/src/wuttafarm/config.py index 0a1e657..fcc8aae 100644 --- a/src/wuttafarm/config.py +++ b/src/wuttafarm/config.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ WuttaFarm config extensions """ @@ -10,20 +30,28 @@ class WuttaFarmConfig(WuttaConfigExtension): """ Config extension for WuttaFarm """ - key = 'wuttafarm' + + key = "wuttafarm" def configure(self, config): # app info - config.setdefault(f'{config.appname}.app_title', "WuttaFarm") - config.setdefault(f'{config.appname}.app_dist', "WuttaFarm") + config.setdefault(f"{config.appname}.app_title", "WuttaFarm") + config.setdefault(f"{config.appname}.app_dist", "WuttaFarm") # app model - config.setdefault(f'{config.appname}.model_spec', 'wuttafarm.db.model') + config.setdefault(f"{config.appname}.model_spec", "wuttafarm.db.model") + + # app handler + config.setdefault( + f"{config.appname}.app.handler", "wuttafarm.app:WuttaFarmAppHandler" + ) # web app menu - config.setdefault(f'{config.appname}.web.menus.handler.spec', - 'wuttafarm.web.menus:WuttaFarmMenuHandler') + config.setdefault( + f"{config.appname}.web.menus.handler.spec", + "wuttafarm.web.menus:WuttaFarmMenuHandler", + ) # web app libcache - #config.setdefault('wuttaweb.static_libcache.module', 'wuttafarm.web.static') + # config.setdefault('wuttaweb.static_libcache.module', 'wuttafarm.web.static') diff --git a/src/wuttafarm/db/model/__init__.py b/src/wuttafarm/db/model/__init__.py index cf3096a..b52d7c8 100644 --- a/src/wuttafarm/db/model/__init__.py +++ b/src/wuttafarm/db/model/__init__.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ WuttaFarm data models """ diff --git a/src/wuttafarm/farmos.py b/src/wuttafarm/farmos.py new file mode 100644 index 0000000..16ec1a6 --- /dev/null +++ b/src/wuttafarm/farmos.py @@ -0,0 +1,57 @@ +# -*- 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 . +# +################################################################################ +""" +farmOS integration handler +""" + +from wuttjamaican.app import GenericHandler + + +class FarmOSHandler(GenericHandler): + """ + Base class and default implementation for the farmOS integration + :term:`handler`. + """ + + def get_farmos_url(self, path=None, require=True): + """ + Returns the base URL for farmOS, or one with ``path`` appended. + + Note that if no path is provided, the final slash will be + stripped from the base URL. + + :param path: Optional path to append to the base URL. + + :param require: If true, an error is raised when base URL + cannot be determined. + + :returns: URL string + """ + base = self.config.get("farmos.url.base", require=require) + if base: + base = base.rstrip("/") + + if path: + path = path.lstrip("/") + return f"{base}/{path}" + + return base diff --git a/src/wuttafarm/web/app.py b/src/wuttafarm/web/app.py index 6664579..b21bd5c 100644 --- a/src/wuttafarm/web/app.py +++ b/src/wuttafarm/web/app.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ WuttaFarm web app """ diff --git a/src/wuttafarm/web/forms/__init__.py b/src/wuttafarm/web/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wuttafarm/web/forms/widgets.py b/src/wuttafarm/web/forms/widgets.py new file mode 100644 index 0000000..047a8cd --- /dev/null +++ b/src/wuttafarm/web/forms/widgets.py @@ -0,0 +1,47 @@ +# -*- 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 . +# +################################################################################ +""" +Custom form widgets for WuttaFarm +""" + +import colander +from deform.widget import Widget +from webhelpers2.html import HTML, tags + + +class AnimalImage(Widget): + """ + Widget to display an image URL for an animal. + + TODO: this should be refactored to a more general name, once more + types of images need to be supported. + """ + + def serialize(self, field, cstruct, **kw): + readonly = kw.get("readonly", self.readonly) + if readonly: + if cstruct in (colander.null, None): + return HTML.tag("span") + + return tags.image(cstruct, "animal image", **kw) + + return super().serialize(field, cstruct, **kw) diff --git a/src/wuttafarm/web/menus.py b/src/wuttafarm/web/menus.py index aaf26c6..e999944 100644 --- a/src/wuttafarm/web/menus.py +++ b/src/wuttafarm/web/menus.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ WuttaFarm Menu """ @@ -12,30 +32,20 @@ class WuttaFarmMenuHandler(base.MenuHandler): """ def make_menus(self, request, **kwargs): - - # nb. the products menu is just an example; you should - # replace it and add more as needed - return [ - self.make_products_menu(request), + self.make_farmos_menu(request), self.make_admin_menu(request, include_people=True), ] - def make_products_menu(self, request): + def make_farmos_menu(self, request): return { - "title": "Products", + "title": "farmOS", "type": "menu", "items": [ { - "title": "Products", - "route": "products", - "perm": "products.list", - }, - {'type': 'sep'}, - { - "title": "Vendors", - "route": "vendors", - "perm": "vendors.list", + "title": "Animals", + "route": "farmos_animals", + "perm": "farmos_animals.list", }, ], } diff --git a/src/wuttafarm/web/static/__init__.py b/src/wuttafarm/web/static/__init__.py index 325e6fa..f61a9e7 100644 --- a/src/wuttafarm/web/static/__init__.py +++ b/src/wuttafarm/web/static/__init__.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ Static assets """ diff --git a/src/wuttafarm/web/subscribers.py b/src/wuttafarm/web/subscribers.py index c3434fe..847be42 100644 --- a/src/wuttafarm/web/subscribers.py +++ b/src/wuttafarm/web/subscribers.py @@ -1,4 +1,24 @@ # -*- 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 . +# +################################################################################ """ Pyramid event subscribers """ @@ -8,9 +28,9 @@ import wuttafarm def add_wuttafarm_to_context(event): renderer_globals = event - renderer_globals['wuttafarm'] = wuttafarm + renderer_globals["wuttafarm"] = wuttafarm def includeme(config): - config.include('wuttaweb.subscribers') - config.add_subscriber(add_wuttafarm_to_context, 'pyramid.events.BeforeRender') + config.include("wuttaweb.subscribers") + config.add_subscriber(add_wuttafarm_to_context, "pyramid.events.BeforeRender") diff --git a/src/wuttafarm/web/templates/base.mako b/src/wuttafarm/web/templates/base.mako new file mode 100644 index 0000000..3e5d544 --- /dev/null +++ b/src/wuttafarm/web/templates/base.mako @@ -0,0 +1,16 @@ +<%inherit file="wuttaweb:templates/base.mako" /> + +<%def name="index_title_controls()"> + ${parent.index_title_controls()} + + % if master is not Undefined and master.listing and farmos_refurl is not Undefined: + + View in farmOS + + % endif + + diff --git a/src/wuttafarm/web/views/__init__.py b/src/wuttafarm/web/views/__init__.py index 09802ca..63ce536 100644 --- a/src/wuttafarm/web/views/__init__.py +++ b/src/wuttafarm/web/views/__init__.py @@ -1,13 +1,41 @@ # -*- 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 . +# +################################################################################ """ WuttaFarm Views """ +from wuttaweb.views import essential + def includeme(config): - # core views for wuttaweb - config.include("wuttaweb.views.essential") + # wuttaweb core + essential.defaults( + config, + **{ + "wuttaweb.views.auth": "wuttafarm.web.views.auth", + "wuttaweb.views.common": "wuttafarm.web.views.common", + } + ) - # TODO: include your own views here - # config.include('wuttafarm.web.views.widgets') + # views for farmOS + config.include("wuttafarm.web.views.farmos") diff --git a/src/wuttafarm/web/views/auth.py b/src/wuttafarm/web/views/auth.py new file mode 100644 index 0000000..ea3207f --- /dev/null +++ b/src/wuttafarm/web/views/auth.py @@ -0,0 +1,58 @@ +# -*- 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 . +# +################################################################################ +""" +Auth views +""" + +from wuttaweb.views import auth as base + + +class AuthView(base.AuthView): + """ + Auth views + """ + + def authenticate_user(self, session, username, password): + if user := super().authenticate_user(session, username, password): + + # nb. auth handler will stash the farmOS oauth2 token in + # the user object, if applicable (i.e. unless the user + # authenticated normally via the app DB). when a token is + # involved we need to put that in the user's web session. + if "farmos_oauth2_token" in user.__dict__: + self.request.session["farmos.oauth2.token"] = user.__dict__.pop( + "farmos_oauth2_token" + ) + + return user + + return None + + +def defaults(config, **kwargs): + local = globals() + AuthView = kwargs.get("AuthView", local["AuthView"]) + base.defaults(config, **{"AuthView": AuthView}) + + +def includeme(config): + defaults(config) diff --git a/src/wuttafarm/web/views/common.py b/src/wuttafarm/web/views/common.py new file mode 100644 index 0000000..bb8a9c9 --- /dev/null +++ b/src/wuttafarm/web/views/common.py @@ -0,0 +1,65 @@ +# -*- 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 . +# +################################################################################ +""" +Common views +""" + +from wuttaweb.views import common as base + + +class CommonView(base.CommonView): + """ + Common views + """ + + def setup_enhance_admin_user(self, user): + """ """ + model = self.app.model + session = self.app.get_session(user) + auth = self.app.get_auth_handler() + + # initialize built-in roles + farm_manager = auth.get_role_farm_manager(session) + farm_manager.notes = "this is meant to mirror the corresponding role in farmOS" + farm_worker = auth.get_role_farm_worker(session) + farm_worker.notes = "this is meant to mirror the corresponding role in farmOS" + farm_viewer = auth.get_role_farm_viewer(session) + farm_viewer.notes = "this is meant to mirror the corresponding role in farmOS" + + site_admin = session.query(model.Role).filter_by(name="Site Admin").first() + if site_admin: + site_admin_perms = [ + "farmos_animals.list", + "farmos_animals.view", + ] + for perm in site_admin_perms: + auth.grant_permission(site_admin, perm) + + +def defaults(config, **kwargs): + local = globals() + CommonView = kwargs.get("CommonView", local["CommonView"]) + base.defaults(config, **{"CommonView": CommonView}) + + +def includeme(config): + defaults(config) diff --git a/src/wuttafarm/web/views/farmos/__init__.py b/src/wuttafarm/web/views/farmos/__init__.py new file mode 100644 index 0000000..df789b3 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/__init__.py @@ -0,0 +1,30 @@ +# -*- 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 to expose farmOS data directly +""" + +from .master import FarmOSMasterView + + +def includeme(config): + config.include("wuttafarm.web.views.farmos.animals") diff --git a/src/wuttafarm/web/views/farmos/animals.py b/src/wuttafarm/web/views/farmos/animals.py new file mode 100644 index 0000000..d765cd4 --- /dev/null +++ b/src/wuttafarm/web/views/farmos/animals.py @@ -0,0 +1,171 @@ +# -*- 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 . +# +################################################################################ +""" +Master view for Farm Animals +""" + +import datetime + +from farmOS import farmOS + +from wuttafarm.web.views.farmos import FarmOSMasterView +from wuttafarm.web.forms.widgets import AnimalImage + + +class AnimalView(FarmOSMasterView): + """ + Master view for Farm Animals + """ + + model_name = "farmos_animal" + model_title = "farmOS Animal" + model_title_plural = "farmOS Animals" + + route_prefix = "farmos_animals" + url_prefix = "/farmOS/animals" + + farmos_refurl_path = "/assets/animal" + + labels = { + "species_breed": "Species / Breed", + "raw_image_url": "Raw Image URL", + "large_image_url": "Large Image URL", + "thumbnail_image_url": "Thumbnail Image URL", + } + + grid_columns = [ + "name", + "species_breed", + "birthdate", + "sex", + "location", + ] + + sort_defaults = "name" + + form_fields = [ + "name", + "species_breed", + "birthdate", + "sex", + "location", + "raw_image_url", + "large_image_url", + "thumbnail_image_url", + "image", + ] + + def get_grid_data(self, columns=None, session=None): + animals = self.farmos_client.resource.get("asset", "animal") + return [self.normalize_animal(a) for a in animals["data"]] + + def configure_grid(self, grid): + g = grid + super().configure_grid(g) + + # name + g.set_link("name") + g.set_searchable("name") + + # birthdate + g.set_renderer("birthdate", "date") + + def get_instance(self): + + animal = self.farmos_client.resource.get_id( + "asset", "animal", self.request.matchdict["uuid"] + ) + + # instance data + data = self.normalize_animal(animal["data"]) + + # add image_url + if relationships := animal["data"].get("relationships"): + if image := relationships.get("image"): + if image["data"]: + image = self.farmos_client.resource.get_id( + "file", "file", image["data"][0]["id"] + ) + data["raw_image_url"] = self.app.get_farmos_url( + image["data"]["attributes"]["uri"]["url"] + ) + # nb. other styles available: medium, wide + data["large_image_url"] = image["data"]["attributes"][ + "image_style_uri" + ]["large"] + data["thumbnail_image_url"] = image["data"]["attributes"][ + "image_style_uri" + ]["thumbnail"] + + return data + + def get_instance_title(self, animal): + return animal["name"] + + def normalize_animal(self, animal): + + birthdate = animal["attributes"]["birthdate"] + if birthdate: + birthdate = datetime.datetime.fromisoformat(birthdate) + birthdate = self.app.localtime(birthdate) + + return { + "uuid": animal["id"], + "drupal_internal_id": animal["attributes"]["drupal_internal__id"], + "name": animal["attributes"]["name"], + "species_breed": "", # TODO + "birthdate": birthdate, + "sex": animal["attributes"]["sex"], + "location": "", # TODO + } + + def configure_form(self, form): + f = form + super().configure_form(f) + animal = f.model_instance + + # image + if url := animal.get("large_image_url"): + f.set_widget("image", AnimalImage()) + f.set_default("image", url) + + def get_xref_buttons(self, animal): + return [ + self.make_button( + "View in farmOS", + primary=True, + url=self.app.get_farmos_url(f"/asset/{animal['drupal_internal_id']}"), + target="_blank", + icon_left="external-link-alt", + ), + ] + + +def defaults(config, **kwargs): + base = globals() + + AnimalView = kwargs.get("AnimalView", base["AnimalView"]) + AnimalView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/src/wuttafarm/web/views/farmos/master.py b/src/wuttafarm/web/views/farmos/master.py new file mode 100644 index 0000000..40f7bfc --- /dev/null +++ b/src/wuttafarm/web/views/farmos/master.py @@ -0,0 +1,65 @@ +# -*- 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 . +# +################################################################################ +""" +Base class for farmOS master views +""" + +from farmOS import farmOS + +from wuttaweb.views import MasterView + + +class FarmOSMasterView(MasterView): + """ + Base class for farmOS master views + """ + + model_key = "uuid" + + creatable = False + editable = False + deletable = False + + filterable = False + sort_on_backend = False + paginate_on_backend = False + + farmos_refurl_path = None + + def __init__(self, request, context=None): + super().__init__(request, context=context) + self.farmos_client = self.get_farmos_client() + + def get_farmos_client(self): + token = self.request.session.get("farmos.oauth2.token") + if not token: + raise self.forbidden() + + url = self.app.get_farmos_url() + return farmOS(url, token=token) + + def get_template_context(self, context): + + if self.listing and self.farmos_refurl_path: + context["farmos_refurl"] = self.app.get_farmos_url(self.farmos_refurl_path) + + return context diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..ce0b62e --- /dev/null +++ b/tasks.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8; -*- +""" +Tasks for WuttaFarm +""" + +import os +import shutil + +from invoke import task + + +@task +def release(c, skip_tests=False): + """ + Release a new version of WuttaFarm + """ + if not skip_tests: + c.run("pytest") + + if os.path.exists("dist"): + shutil.rmtree("dist") + + c.run("python -m build --sdist") + c.run("twine upload dist/*")