Compare commits
No commits in common. "9bd1c07193bb296d44cb6e803d8b38fa31b74977" and "ec06c299d5e06f0038f242ba16600cd322402f8a" have entirely different histories.
9bd1c07193
...
ec06c299d5
16 changed files with 19 additions and 609 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -5,27 +5,6 @@ All notable changes to WuttaFarm will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.1.3 (2026-02-06)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- fix a couple more edge cases around oauth2 token refresh
|
|
||||||
|
|
||||||
## v0.1.2 (2026-02-06)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- add support for farmOS/OAuth2 Authorization Code grant/workflow
|
|
||||||
|
|
||||||
## v0.1.1 (2026-02-05)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- preserve oauth2 token so auto-refresh works correctly
|
|
||||||
- customize app installer to configure farmos_url
|
|
||||||
- add some more info when viewing animal
|
|
||||||
- require minimum version for wuttaweb
|
|
||||||
|
|
||||||
## v0.1.0 (2026-02-03)
|
## v0.1.0 (2026-02-03)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaFarm"
|
name = "WuttaFarm"
|
||||||
version = "0.1.3"
|
version = "0.1.0"
|
||||||
description = "Web app to integrate with and extend farmOS"
|
description = "Web app to integrate with and extend farmOS"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|
@ -31,7 +31,7 @@ license = {text = "GNU General Public License v3"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"farmOS",
|
"farmOS",
|
||||||
"psycopg2",
|
"psycopg2",
|
||||||
"WuttaWeb[continuum]>=0.27.2",
|
"WuttaWeb[continuum]",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
|
default_auth_handler_spec = "wuttafarm.auth:WuttaFarmAuthHandler"
|
||||||
default_install_handler_spec = "wuttafarm.install:WuttaFarmInstallHandler"
|
|
||||||
|
|
||||||
def get_farmos_handler(self):
|
def get_farmos_handler(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -43,7 +42,7 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
if "farmos" not in self.handlers:
|
if "farmos" not in self.handlers:
|
||||||
spec = self.config.get(
|
spec = self.config.get(
|
||||||
f"{self.appname}.farmos_handler",
|
f"{self.appname}.farmos_handler",
|
||||||
default="wuttafarm.farmos.handler:FarmOSHandler",
|
default="wuttafarm.farmos:FarmOSHandler",
|
||||||
)
|
)
|
||||||
factory = self.load_object(spec)
|
factory = self.load_object(spec)
|
||||||
self.handlers["farmos"] = factory(self.config)
|
self.handlers["farmos"] = factory(self.config)
|
||||||
|
|
@ -52,15 +51,7 @@ class WuttaFarmAppHandler(base.AppHandler):
|
||||||
def get_farmos_url(self, *args, **kwargs):
|
def get_farmos_url(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get a farmOS URL. This is a convenience wrapper around
|
Get a farmOS URL. This is a convenience wrapper around
|
||||||
:meth:`~wuttafarm.farmos.handler.FarmOSHandler.get_farmos_url()`.
|
:meth:`~wuttafarm.farmos.FarmOSHandler.get_farmos_url()`.
|
||||||
"""
|
"""
|
||||||
handler = self.get_farmos_handler()
|
handler = self.get_farmos_handler()
|
||||||
return handler.get_farmos_url(*args, **kwargs)
|
return handler.get_farmos_url(*args, **kwargs)
|
||||||
|
|
||||||
def get_farmos_client(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Get a farmOS client. This is a convenience wrapper around
|
|
||||||
:meth:`~wuttafarm.farmos.handler.FarmOSHandler.get_farmos_client()`.
|
|
||||||
"""
|
|
||||||
handler = self.get_farmos_handler()
|
|
||||||
return handler.get_farmos_client(*args, **kwargs)
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ Auth handler for use with farmOS
|
||||||
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from farmOS import farmOS
|
||||||
from oauthlib.oauth2.rfc6749.errors import InvalidGrantError
|
from oauthlib.oauth2.rfc6749.errors import InvalidGrantError
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
|
@ -88,7 +89,8 @@ class WuttaFarmAuthHandler(AuthHandler):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_farmos_oauth2_token(self, username, password):
|
def get_farmos_oauth2_token(self, username, password):
|
||||||
client = self.app.get_farmos_client()
|
url = self.app.get_farmos_url()
|
||||||
|
client = farmOS(url)
|
||||||
try:
|
try:
|
||||||
return client.authorize(username=username, password=password)
|
return client.authorize(username=username, password=password)
|
||||||
except InvalidGrantError:
|
except InvalidGrantError:
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@
|
||||||
farmOS integration handler
|
farmOS integration handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from farmOS import farmOS
|
|
||||||
|
|
||||||
from wuttjamaican.app import GenericHandler
|
from wuttjamaican.app import GenericHandler
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,14 +32,6 @@ class FarmOSHandler(GenericHandler):
|
||||||
:term:`handler`.
|
:term:`handler`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_farmos_client(self, hostname=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns a new farmOS API client.
|
|
||||||
"""
|
|
||||||
if not hostname:
|
|
||||||
hostname = self.get_farmos_url()
|
|
||||||
return farmOS(hostname, **kwargs)
|
|
||||||
|
|
||||||
def get_farmos_url(self, path=None, require=True):
|
def get_farmos_url(self, path=None, require=True):
|
||||||
"""
|
"""
|
||||||
Returns the base URL for farmOS, or one with ``path`` appended.
|
Returns the base URL for farmOS, or one with ``path`` appended.
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
# -*- 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/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Install handler for WuttaFarm
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wuttjamaican import install as base
|
|
||||||
|
|
||||||
|
|
||||||
class WuttaFarmInstallHandler(base.InstallHandler):
|
|
||||||
"""
|
|
||||||
Custom install handler for WuttaFarm
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_paths = ["wuttafarm:installer-templates"]
|
|
||||||
|
|
||||||
def prompt_user_for_context(self):
|
|
||||||
context = super().prompt_user_for_context()
|
|
||||||
|
|
||||||
context["farmos_url"] = self.prompt_generic("farmos_url", required=True)
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#!/bin/sh -e
|
|
||||||
<%text>##################################################</%text>
|
|
||||||
#
|
|
||||||
# ${app_title} - upgrade script
|
|
||||||
#
|
|
||||||
<%text>##################################################</%text>
|
|
||||||
|
|
||||||
if [ "$1" = "--verbose" ]; then
|
|
||||||
VERBOSE='--verbose'
|
|
||||||
QUIET=
|
|
||||||
else
|
|
||||||
VERBOSE=
|
|
||||||
QUIET='--quiet'
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd ${envdir}
|
|
||||||
|
|
||||||
PIP='bin/pip'
|
|
||||||
ALEMBIC='bin/alembic'
|
|
||||||
|
|
||||||
# upgrade pip and friends
|
|
||||||
$PIP install $QUIET --disable-pip-version-check --upgrade pip
|
|
||||||
$PIP install $QUIET --upgrade setuptools wheel
|
|
||||||
|
|
||||||
# upgrade app proper
|
|
||||||
$PIP install $QUIET --upgrade --upgrade-strategy eager '${pypi_name}'
|
|
||||||
|
|
||||||
# migrate schema
|
|
||||||
$ALEMBIC -c app/wutta.conf upgrade heads
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
## -*- mode: conf; -*-
|
|
||||||
|
|
||||||
<%text>############################################################</%text>
|
|
||||||
#
|
|
||||||
# ${app_title} - web app config
|
|
||||||
#
|
|
||||||
<%text>############################################################</%text>
|
|
||||||
|
|
||||||
|
|
||||||
<%text>##############################</%text>
|
|
||||||
# wutta
|
|
||||||
<%text>##############################</%text>
|
|
||||||
|
|
||||||
${self.section_wutta_config()}
|
|
||||||
|
|
||||||
|
|
||||||
<%text>##############################</%text>
|
|
||||||
# pyramid
|
|
||||||
<%text>##############################</%text>
|
|
||||||
|
|
||||||
${self.section_app_main()}
|
|
||||||
|
|
||||||
${self.section_server_main()}
|
|
||||||
|
|
||||||
|
|
||||||
<%text>##############################</%text>
|
|
||||||
# logging
|
|
||||||
<%text>##############################</%text>
|
|
||||||
|
|
||||||
${self.sectiongroup_logging()}
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
## section templates below
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
<%def name="section_wutta_config()">
|
|
||||||
[wutta.config]
|
|
||||||
require = %(here)s/wutta.conf
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_app_main()">
|
|
||||||
[app:main]
|
|
||||||
#use = egg:wuttaweb
|
|
||||||
use = egg:${egg_name}
|
|
||||||
|
|
||||||
pyramid.reload_templates = true
|
|
||||||
pyramid.debug_all = true
|
|
||||||
pyramid.default_locale_name = en
|
|
||||||
#pyramid.includes = pyramid_debugtoolbar
|
|
||||||
|
|
||||||
beaker.session.type = file
|
|
||||||
beaker.session.data_dir = %(here)s/cache/sessions/data
|
|
||||||
beaker.session.lock_dir = %(here)s/cache/sessions/lock
|
|
||||||
beaker.session.secret = ${beaker_secret}
|
|
||||||
beaker.session.key = ${beaker_key}
|
|
||||||
|
|
||||||
exclog.extra_info = true
|
|
||||||
|
|
||||||
# required for wuttaweb
|
|
||||||
wutta.config = %(__file__)s
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_server_main()">
|
|
||||||
[server:main]
|
|
||||||
use = egg:waitress#main
|
|
||||||
host = ${pyramid_host}
|
|
||||||
port = ${pyramid_port}
|
|
||||||
|
|
||||||
# NOTE: this is needed for local reverse proxy stuff to work with HTTPS
|
|
||||||
# https://docs.pylonsproject.org/projects/waitress/en/latest/reverse-proxy.html
|
|
||||||
# https://docs.pylonsproject.org/projects/waitress/en/latest/arguments.html
|
|
||||||
trusted_proxy = 127.0.0.1
|
|
||||||
trusted_proxy_headers = x-forwarded-for x-forwarded-host x-forwarded-proto x-forwarded-port
|
|
||||||
clear_untrusted_proxy_headers = True
|
|
||||||
|
|
||||||
# TODO: leave this empty if proxy serves as root site, e.g. https://wutta.example.com/
|
|
||||||
# url_prefix =
|
|
||||||
|
|
||||||
# TODO: or, if proxy serves as subpath of root site, e.g. https://wutta.example.com/backend/
|
|
||||||
# url_prefix = /backend
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="sectiongroup_logging()">
|
|
||||||
[handler_console]
|
|
||||||
level = INFO
|
|
||||||
|
|
||||||
[handler_file]
|
|
||||||
args = (${repr(os.path.join(appdir, 'log', 'web.log'))}, 'a', 1000000, 100, 'utf_8')
|
|
||||||
</%def>
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
## -*- mode: conf; -*-
|
|
||||||
|
|
||||||
<%text>############################################################</%text>
|
|
||||||
#
|
|
||||||
# ${app_title} - base config
|
|
||||||
#
|
|
||||||
<%text>############################################################</%text>
|
|
||||||
|
|
||||||
|
|
||||||
[farmos]
|
|
||||||
url.base = ${farmos_url}
|
|
||||||
|
|
||||||
|
|
||||||
<%text>##############################</%text>
|
|
||||||
# wutta
|
|
||||||
<%text>##############################</%text>
|
|
||||||
|
|
||||||
${self.section_wutta()}
|
|
||||||
|
|
||||||
${self.section_wutta_config()}
|
|
||||||
|
|
||||||
${self.section_wutta_db()}
|
|
||||||
|
|
||||||
${self.section_wutta_mail()}
|
|
||||||
|
|
||||||
${self.section_wutta_upgrades()}
|
|
||||||
|
|
||||||
|
|
||||||
<%text>##############################</%text>
|
|
||||||
# alembic
|
|
||||||
<%text>##############################</%text>
|
|
||||||
|
|
||||||
${self.section_alembic()}
|
|
||||||
|
|
||||||
|
|
||||||
<%text>##############################</%text>
|
|
||||||
# logging
|
|
||||||
<%text>##############################</%text>
|
|
||||||
|
|
||||||
${self.sectiongroup_logging()}
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
## section templates below
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
<%def name="section_wutta()">
|
|
||||||
[wutta]
|
|
||||||
#app_title = ${app_title}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_wutta_config()">
|
|
||||||
[wutta.config]
|
|
||||||
#require = /etc/wutta/wutta.conf
|
|
||||||
configure_logging = true
|
|
||||||
usedb = true
|
|
||||||
preferdb = true
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_wutta_db()">
|
|
||||||
[wutta.db]
|
|
||||||
default.url = ${db_url}
|
|
||||||
|
|
||||||
% if wants_continuum:
|
|
||||||
[wutta_continuum]
|
|
||||||
enable_versioning = true
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_wutta_mail()">
|
|
||||||
[wutta.mail]
|
|
||||||
|
|
||||||
# this is the global email shutoff switch
|
|
||||||
#send_emails = false
|
|
||||||
|
|
||||||
# recommended setup is to always talk to postfix on localhost and then
|
|
||||||
# it can handle any need complexities, e.g. sending to relay
|
|
||||||
smtp.server = localhost
|
|
||||||
|
|
||||||
# by default only email templates from wuttjamaican are used
|
|
||||||
templates = wuttjamaican:templates/mail
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
## # this is the "default" email profile, from which all others initially
|
|
||||||
## # inherit, but most/all profiles will override these values
|
|
||||||
## default.prefix = [${app_title}]
|
|
||||||
## default.from = wutta@localhost
|
|
||||||
## default.to = root@localhost
|
|
||||||
# nb. in test environment it can be useful to disable by default, and
|
|
||||||
# then selectively enable certain (e.g. feedback, upgrade) emails
|
|
||||||
#default.enabled = false
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_wutta_upgrades()">
|
|
||||||
## TODO
|
|
||||||
## [wutta.upgrades]
|
|
||||||
## command = ${os.path.join(appdir, 'upgrade.sh')} --verbose
|
|
||||||
## files = ${os.path.join(appdir, 'data', 'upgrades')}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="section_alembic()">
|
|
||||||
[alembic]
|
|
||||||
script_location = wuttjamaican.db:alembic
|
|
||||||
% if wants_continuum:
|
|
||||||
version_locations = ${pkg_name}.db:alembic/versions wutta_continuum.db:alembic/versions wuttjamaican.db:alembic/versions
|
|
||||||
% else:
|
|
||||||
version_locations = ${pkg_name}.db:alembic/versions wuttjamaican.db:alembic/versions
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="sectiongroup_logging()">
|
|
||||||
[loggers]
|
|
||||||
keys = root, beaker, exc_logger, sqlalchemy, txn
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = file, console, email
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic, console
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
handlers = file, console
|
|
||||||
level = DEBUG
|
|
||||||
|
|
||||||
[logger_beaker]
|
|
||||||
qualname = beaker
|
|
||||||
handlers =
|
|
||||||
level = INFO
|
|
||||||
|
|
||||||
[logger_exc_logger]
|
|
||||||
qualname = exc_logger
|
|
||||||
handlers = email
|
|
||||||
level = ERROR
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
handlers =
|
|
||||||
# handlers = file
|
|
||||||
# level = INFO
|
|
||||||
|
|
||||||
[logger_txn]
|
|
||||||
qualname = txn
|
|
||||||
handlers =
|
|
||||||
level = INFO
|
|
||||||
|
|
||||||
[handler_file]
|
|
||||||
class = handlers.RotatingFileHandler
|
|
||||||
args = (${repr(os.path.join(appdir, 'log', 'wutta.log'))}, 'a', 1000000, 100, 'utf_8')
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
formatter = console
|
|
||||||
# formatter = generic
|
|
||||||
# level = INFO
|
|
||||||
# level = WARNING
|
|
||||||
|
|
||||||
[handler_email]
|
|
||||||
class = handlers.SMTPHandler
|
|
||||||
args = ('localhost', 'wutta@localhost', ['root@localhost'], "[${app_title}] Logging")
|
|
||||||
formatter = generic
|
|
||||||
level = ERROR
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
|
|
||||||
datefmt = %Y-%m-%d %H:%M:%S
|
|
||||||
|
|
||||||
[formatter_console]
|
|
||||||
format = %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
|
|
||||||
</%def>
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
<%inherit file="wuttaweb:templates/auth/login.mako" />
|
|
||||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
|
||||||
|
|
||||||
<%def name="page_content()">
|
|
||||||
<div class="wutta-page-content">
|
|
||||||
<div class="wutta-logo">${base_meta.full_logo(image_url or None)}</div>
|
|
||||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
${form.render_vue_tag()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="has-text-italic">
|
|
||||||
- OR -
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<wutta-button type="is-primary"
|
|
||||||
tag="a"
|
|
||||||
href="${url('farmos_oauth_login')}"
|
|
||||||
icon-left="user"
|
|
||||||
label="Login via farmOS / OAuth2"
|
|
||||||
once />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</%def>
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
# -*- 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/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Misc. utilities for web app
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def save_farmos_oauth2_token(request, token):
|
|
||||||
"""
|
|
||||||
Common logic for saving the given OAuth2 token within the user
|
|
||||||
session. This function is called from 2 places:
|
|
||||||
|
|
||||||
* :meth:`wuttafarm.web.views.auth.farmos_oauth_callback()`
|
|
||||||
* :meth:`wuttafarm.web.views.farmos.master.FarmOSMasterView.get_farmos_client()`
|
|
||||||
"""
|
|
||||||
# nb. we pretend the token expires 1 minute early, to avoid edge
|
|
||||||
# cases around token refresh
|
|
||||||
token["expires_at"] -= 60
|
|
||||||
|
|
||||||
# save token to user session
|
|
||||||
request.session["farmos.oauth2.token"] = token
|
|
||||||
|
|
@ -23,14 +23,7 @@
|
||||||
Auth views
|
Auth views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from oauthlib.oauth2 import AccessDeniedError
|
|
||||||
from requests_oauthlib import OAuth2Session
|
|
||||||
|
|
||||||
from wuttaweb.views import auth as base
|
from wuttaweb.views import auth as base
|
||||||
from wuttaweb.auth import login_user
|
|
||||||
from wuttaweb.db import Session
|
|
||||||
|
|
||||||
from wuttafarm.web.util import save_farmos_oauth2_token
|
|
||||||
|
|
||||||
|
|
||||||
class AuthView(base.AuthView):
|
class AuthView(base.AuthView):
|
||||||
|
|
@ -54,93 +47,6 @@ class AuthView(base.AuthView):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_farmos_oauth2_session(self):
|
|
||||||
return OAuth2Session(
|
|
||||||
client_id="farm",
|
|
||||||
scope="farm_manager",
|
|
||||||
redirect_uri=self.request.route_url("farmos_oauth_callback"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def farmos_oauth_login(self):
|
|
||||||
"""
|
|
||||||
View to initiate OAuth2 workflow.
|
|
||||||
"""
|
|
||||||
oauth = self.get_farmos_oauth2_session()
|
|
||||||
auth_url, state = oauth.authorization_url(
|
|
||||||
self.app.get_farmos_url("/oauth/authorize")
|
|
||||||
)
|
|
||||||
return self.redirect(auth_url)
|
|
||||||
|
|
||||||
def farmos_oauth_callback(self):
|
|
||||||
"""
|
|
||||||
View for OAuth2 workflow, provided as redirect URL when
|
|
||||||
authorizing.
|
|
||||||
"""
|
|
||||||
session = Session()
|
|
||||||
auth = self.app.get_auth_handler()
|
|
||||||
oauth = self.get_farmos_oauth2_session()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# get oauth token from farmOS
|
|
||||||
token = oauth.fetch_token(
|
|
||||||
self.app.get_farmos_url("/oauth/token"),
|
|
||||||
authorization_response=self.request.current_route_url(),
|
|
||||||
include_client_id=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
except AccessDeniedError:
|
|
||||||
self.request.session.flash("Access to farmOS was denied.", "error")
|
|
||||||
return self.redirect(self.request.route_url("login"))
|
|
||||||
|
|
||||||
# save token in user session
|
|
||||||
save_farmos_oauth2_token(self.request, token)
|
|
||||||
|
|
||||||
# nb. must give a *copy* of the token to farmOS client, since
|
|
||||||
# it will mutate it in-place and we don't want that to happen
|
|
||||||
# for our original copy in the user session. (otherwise the
|
|
||||||
# auto-refresh will not work correctly for subsequent calls.)
|
|
||||||
token = dict(token)
|
|
||||||
|
|
||||||
# get (or create) native app user
|
|
||||||
farmos_client = self.app.get_farmos_client(token=token)
|
|
||||||
info = farmos_client.info()
|
|
||||||
farmos_user = farmos_client.resource.get_id(
|
|
||||||
"user", "user", info["meta"]["links"]["me"]["meta"]["id"]
|
|
||||||
)
|
|
||||||
user = auth.get_or_make_farmos_user(
|
|
||||||
session, farmos_user["data"]["attributes"]["name"]
|
|
||||||
)
|
|
||||||
if not user:
|
|
||||||
self.request.session.flash(
|
|
||||||
"farmOS authentication was successful, but user is "
|
|
||||||
"not allowed login to {self.app.get_node_title()}.",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
return self.redirect(self.request.route_url("login"))
|
|
||||||
|
|
||||||
# delare user is logged in
|
|
||||||
headers = login_user(self.request, user)
|
|
||||||
referrer = self.request.get_referrer()
|
|
||||||
return self.redirect(referrer, headers=headers)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def defaults(cls, config):
|
|
||||||
cls._auth_defaults(config)
|
|
||||||
cls._wuttafarm_defaults(config)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _wuttafarm_defaults(cls, config):
|
|
||||||
|
|
||||||
# farmos oauth login
|
|
||||||
config.add_route("farmos_oauth_login", "/farmos/oauth/login")
|
|
||||||
config.add_view(cls, attr="farmos_oauth_login", route_name="farmos_oauth_login")
|
|
||||||
|
|
||||||
# farmos oauth callback
|
|
||||||
config.add_route("farmos_oauth_callback", "/farmos/oauth/callback")
|
|
||||||
config.add_view(
|
|
||||||
cls, attr="farmos_oauth_callback", route_name="farmos_oauth_callback"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
local = globals()
|
local = globals()
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ Master view for Farm Animals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import colander
|
from farmOS import farmOS
|
||||||
|
|
||||||
from wuttafarm.web.views.farmos import FarmOSMasterView
|
from wuttafarm.web.views.farmos import FarmOSMasterView
|
||||||
from wuttafarm.web.forms.widgets import AnimalImage
|
from wuttafarm.web.forms.widgets import AnimalImage
|
||||||
|
|
@ -46,8 +46,7 @@ class AnimalView(FarmOSMasterView):
|
||||||
farmos_refurl_path = "/assets/animal"
|
farmos_refurl_path = "/assets/animal"
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"is_castrated": "Castrated",
|
"species_breed": "Species / Breed",
|
||||||
"location_name": "Current Location",
|
|
||||||
"raw_image_url": "Raw Image URL",
|
"raw_image_url": "Raw Image URL",
|
||||||
"large_image_url": "Large Image URL",
|
"large_image_url": "Large Image URL",
|
||||||
"thumbnail_image_url": "Thumbnail Image URL",
|
"thumbnail_image_url": "Thumbnail Image URL",
|
||||||
|
|
@ -55,24 +54,20 @@ class AnimalView(FarmOSMasterView):
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
"name",
|
"name",
|
||||||
|
"species_breed",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"sex",
|
"sex",
|
||||||
"is_castrated",
|
"location",
|
||||||
"status",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_defaults = "name"
|
sort_defaults = "name"
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"animal_type_name",
|
"species_breed",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"sex",
|
"sex",
|
||||||
"is_castrated",
|
"location",
|
||||||
"status",
|
|
||||||
"owners",
|
|
||||||
"location_name",
|
|
||||||
"notes",
|
|
||||||
"raw_image_url",
|
"raw_image_url",
|
||||||
"large_image_url",
|
"large_image_url",
|
||||||
"thumbnail_image_url",
|
"thumbnail_image_url",
|
||||||
|
|
@ -103,38 +98,8 @@ class AnimalView(FarmOSMasterView):
|
||||||
# instance data
|
# instance data
|
||||||
data = self.normalize_animal(animal["data"])
|
data = self.normalize_animal(animal["data"])
|
||||||
|
|
||||||
|
# add image_url
|
||||||
if relationships := animal["data"].get("relationships"):
|
if relationships := animal["data"].get("relationships"):
|
||||||
|
|
||||||
# add animal type
|
|
||||||
if animal_type := relationships.get("animal_type"):
|
|
||||||
if animal_type["data"]:
|
|
||||||
animal_type = self.farmos_client.resource.get_id(
|
|
||||||
"taxonomy_term", "animal_type", animal_type["data"]["id"]
|
|
||||||
)
|
|
||||||
data["animal_type_name"] = animal_type["data"]["attributes"]["name"]
|
|
||||||
|
|
||||||
# add location
|
|
||||||
if location := relationships.get("location"):
|
|
||||||
if location["data"]:
|
|
||||||
location = self.farmos_client.resource.get_id(
|
|
||||||
"asset", "structure", location["data"][0]["id"]
|
|
||||||
)
|
|
||||||
data["location_name"] = location["data"]["attributes"]["name"]
|
|
||||||
|
|
||||||
# add owners
|
|
||||||
if owner := relationships.get("owner"):
|
|
||||||
owners = []
|
|
||||||
for owner_data in owner["data"]:
|
|
||||||
owners.append(
|
|
||||||
self.farmos_client.resource.get_id(
|
|
||||||
"user", "user", owner_data["id"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
data["owners"] = ", ".join(
|
|
||||||
[o["data"]["attributes"]["display_name"] for o in owners]
|
|
||||||
)
|
|
||||||
|
|
||||||
# add image urls
|
|
||||||
if image := relationships.get("image"):
|
if image := relationships.get("image"):
|
||||||
if image["data"]:
|
if image["data"]:
|
||||||
image = self.farmos_client.resource.get_id(
|
image = self.farmos_client.resource.get_id(
|
||||||
|
|
@ -170,10 +135,7 @@ class AnimalView(FarmOSMasterView):
|
||||||
"species_breed": "", # TODO
|
"species_breed": "", # TODO
|
||||||
"birthdate": birthdate,
|
"birthdate": birthdate,
|
||||||
"sex": animal["attributes"]["sex"],
|
"sex": animal["attributes"]["sex"],
|
||||||
"is_castrated": animal["attributes"]["is_castrated"],
|
|
||||||
"location": "", # TODO
|
"location": "", # TODO
|
||||||
"status": animal["attributes"]["status"],
|
|
||||||
"notes": animal["attributes"]["notes"]["value"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
|
|
@ -181,12 +143,6 @@ class AnimalView(FarmOSMasterView):
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
animal = f.model_instance
|
animal = f.model_instance
|
||||||
|
|
||||||
# is_castrated
|
|
||||||
f.set_node("is_castrated", colander.Boolean())
|
|
||||||
|
|
||||||
# notes
|
|
||||||
f.set_widget("notes", "notes")
|
|
||||||
|
|
||||||
# image
|
# image
|
||||||
if url := animal.get("large_image_url"):
|
if url := animal.get("large_image_url"):
|
||||||
f.set_widget("image", AnimalImage())
|
f.set_widget("image", AnimalImage())
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@
|
||||||
Base class for farmOS master views
|
Base class for farmOS master views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
from farmOS import farmOS
|
||||||
|
|
||||||
from wuttafarm.web.util import save_farmos_oauth2_token
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
class FarmOSMasterView(MasterView):
|
class FarmOSMasterView(MasterView):
|
||||||
|
|
@ -54,16 +54,8 @@ class FarmOSMasterView(MasterView):
|
||||||
if not token:
|
if not token:
|
||||||
raise self.forbidden()
|
raise self.forbidden()
|
||||||
|
|
||||||
# nb. must give a *copy* of the token to farmOS client, since
|
url = self.app.get_farmos_url()
|
||||||
# it will mutate it in-place and we don't want that to happen
|
return farmOS(url, token=token)
|
||||||
# for our original copy in the user session. (otherwise the
|
|
||||||
# auto-refresh will not work correctly for subsequent calls.)
|
|
||||||
token = dict(token)
|
|
||||||
|
|
||||||
def token_updater(token):
|
|
||||||
save_farmos_oauth2_token(self.request, token)
|
|
||||||
|
|
||||||
return self.app.get_farmos_client(token=token, token_updater=token_updater)
|
|
||||||
|
|
||||||
def get_template_context(self, context):
|
def get_template_context(self, context):
|
||||||
|
|
||||||
|
|
|
||||||
4
tasks.py
4
tasks.py
|
|
@ -15,9 +15,7 @@ def release(c, skip_tests=False):
|
||||||
Release a new version of WuttaFarm
|
Release a new version of WuttaFarm
|
||||||
"""
|
"""
|
||||||
if not skip_tests:
|
if not skip_tests:
|
||||||
# TODO
|
c.run("pytest")
|
||||||
# c.run("pytest")
|
|
||||||
pass
|
|
||||||
|
|
||||||
if os.path.exists("dist"):
|
if os.path.exists("dist"):
|
||||||
shutil.rmtree("dist")
|
shutil.rmtree("dist")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue