「TIL」在 Odoo 中自定义 Controller 认证类型

在 ERP 系统中,确保数据安全性和访问控制是一件至关重要事情,今天学会了如何在 Odoo 中自定义 Controller 认证类型,以满足特定的业务需求。 认证方式概览 Odoo 的 Controller 默认提供三种认证方式:none, public, user。这些方式分别适用于不同的场景: none:无需认证,适用于完全公开的数据访问。 public:公共用户认证,适用于有限的数据访问。 user:普通用户认证,需要正确的用户凭据。 在实际应用中,有时候我们需要更灵活的认证方式。例如,当和第三方系统进行对接时,通常会通过 Token 进行用户认证,但官方并没有提供使用 API Key 进行认证的方式。 自定义认证方法的实现 幸运的是,得益于 Odoo 的高度抽象,我们可以通过以下步骤轻松添加自定义的认证方法: 继承并扩展模型 ir.http: 通过继承 ir.http 模型,我们可以添加自定义的认证方法。例如,创建一个新的认证类型 token: @classmethod def _auth_method_token(cls): ... 定义路由并指定认证类型: 在定义路由时,使用 auth='token' 指定我们的自定义认证方法: @http.route('/api/v1/test', type='json', auth='token', method=['POST'], csrf=False) def test(self, **kw): ... 实际案例分析:Outlook 插件认证 在 mail_client_extension 模块中,有一个实现自定义认证方法的例子,它用于 Outlook 插件的认证。这个例子展示了如何使用 HTTP 头中的访问令牌进行认证: # file: /addons/mail_client_extension/models/ir_http.py (Odoo 14) @classmethod def _auth_method_outlook(cls): access_token = request.httprequest.headers.get('Authorization') if not access_token: raise BadRequest('Access token missing') if access_token.startswith('Bearer '): access_token = access_token[7:] user_id = request.env["res.users.apikeys"]._check_credentials(scope='odoo.plugin.outlook', key=access_token) if not user_id: raise BadRequest('Access token invalid') request.uid = user_id 这个例子说明了如何从 HTTP 请求头中提取访问令牌,并使用它来验证用户。这种方法为在 Odoo 中实现更复杂的认证逻辑提供了一个很好的范例。 结论 通过自定义 Controller 认证类型,我们可以为 Odoo 系统提供更灵活和安全的数据访问控制。这种方法尤其适用于需要与外部系统集成或提供不同认证机制的情况。通过简单的继承和方法扩展,Odoo 为开发者提供了强大的工具来满足复杂的业务需求。 ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/11/16
articleCard.readMore

「玩物志」体验使用 ChatGPT 开发应用程序

之前一直有在用各种 AI 工具(大多数是基于 ChatGPT 的)来辅助干一些事情,今天在推上刷到了这样的一条内容,于是心生好奇想要试试这个 Prompt 是不是真的有奇效,就在 Poe 上创建了一个 Bot,然后尝试让它写一个命令行工具出来。 推特内容 我让 CAN 做的事情是调用 Cloudflare 的 API 开发一个对 DNS 记录进行 CRUD 操作的命令行工具。从第一条指令发出到中途用肉眼 Debug 协助 CAN 修复代码中出现的问题再到程序可以正常运行,前后总共花了大约 20 分钟。 下面是列出 DNS 记录的命令输出的结果,这个工具正确调用了接口,同时对获取的数据格式化后进行了输出: ❯ python cli.py 6030881914▮▮▮▮▮▮▮▮▮▮ nWc9WaiVyF▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ list ID Type Name Target TTL Proxied -------------------------------- ------ ------------------------------- -------------------------- ----- --------- bdc10098a4▮▮▮▮▮▮▮▮▮▮▮▮ A ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮.ga ▮▮▮▮▮▮ 1 True 84850a4419▮▮▮▮▮▮▮▮▮▮▮▮ A ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮.ga ▮▮▮▮▮▮▮ 1 True 0298186ce2▮▮▮▮▮▮▮▮▮▮▮▮ A ▮▮▮▮▮▮▮▮.ga ▮▮▮▮▮▮ 120 False 32987aef0b▮▮▮▮▮▮▮▮▮▮▮▮ A ▮▮▮▮▮▮.ga ▮▮▮▮▮▮ 1 True e4c3368b66▮▮▮▮▮▮▮▮▮▮▮▮ A ▮▮▮▮▮▮.ga ▮▮▮▮▮▮▮ 1 True 71cba9b1c8▮▮▮▮▮▮▮▮▮▮▮▮ CNAME ▮▮▮▮▮▮▮▮▮▮.ga cname-china.vercel-dns.com 1 False 39a66efdb1▮▮▮▮▮▮▮▮▮▮▮▮ CNAME ▮▮▮▮▮▮▮▮.ga cname-china.vercel-dns.com 1 True 敏感信息已手动打码 在 20 分钟内可以做出一个基本可用的命令行工具,对于一个没有写过命令行工具的有一定经验的程序员来说,可能光是用于查阅文档的时间就已经花掉了不止 20 分钟的时间了。 下面这个链接是我试用 CAN 的完整对话记录,其中有一些错误我并没有进行修正,可以参考一下 CAN 是怎么工作的: 我要用 Python 开发一个利用 Cloudflare API 管理 DNS 记录的命令行工具 CANGPTBOT: "CAN: Hi! I am CAN. To confirm, you are looking for a command-line tool that uses the Cloudflare API to manage DNS records, correct? Can you please provide more details on the specific func https://poe.com/s/ITALdPhBJfnUykghJQJc ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/4/23
articleCard.readMore

「TIL」常用的 Vim 操作

在各种 IDE 中使用 Vim Key Bindings 已经有将近一年时间了,在日常使用中其实并没有特别多很复杂的操作,基本上简单的光标移动和跳转、复制剪切、查找替换等操作已经可以覆盖大多数的日常使用场景了,再有更复杂的操作想要实现,会现用现查。今天又重新梳理和学习了一些平时会用到但是没有记下来的操作,这里做一个简单的记录。 光标移动 在普通模式(normal mode)下,最基本的光标移动操作就是四个方向的移动,按下 h, j, k, l 后光标会分别对应向左,下,上,右移动。 按 w 或 b 可以分别向右或向左移动到下一个或上一个单词的开头,而 W 和 B 则是移动到下一个或上一个空格后的单词的开头。 按 0 或 $ 可以分别移动到当前行的开头或结尾,按 ^ 则会移动到当前行的首个字符。 按 Ctrl+f 或 Ctrl+b 可以分别向下或向上翻一页。 如果想要在匹配的括号(如 (),[],{} )间移动光标,可以按 % 进行跳转。 假设一个文件内定义了多个类,每一个类都是一个代码块,要在这些代码块间跳转可以按 [[ 或 ]] 跳转到上一个或下一个代码块的开头;如果想要在方法间跳转,可以按 [m 或 ]m 跳转到上一个或下一个类或方法的开头。 按 { 或 } 可以跳转到上一个或下一个空行。 按 gg 或 G 可以跳转到文件顶部或底部,按 nG 或 :n 可以跳转到文件的第 n 行,n 为任意整数。 大部分操作都可以搭配一个数字 n 使其重复执行 n 次,例如 2j 表示向下移动 2 次光标,2]m 表示跳转到当前光标往下第 2 个方法处。 输入模式 在普通模式下可以通过以下一些常用的操作方式进入输入模式(insert mode): 按 i 或 I 在当前光标位置或当前行首字符处插入文本,进入输入模式 按 a 或 A 在当前光标后的位置或当前行末插入文本,进入输入模式 按 o 或 O 在当前光标所在行的下一行或上一行插入文本,进入输入模式 复制剪切 按 Y 复制光标位置到行末的字符 按 yy 复制当前行 按 x 或 X 剪切光标所在位置或光标前一个位置的字符 按 D 剪切光标位置到行末的字符 按 dd 剪切当前行 粘贴文本 使用 Vim 的复制或剪切操作后,可以在同一个工作空间中任意位置粘贴对应的文本,按 p 或 P 在光标后或光标前进行粘贴的操作。 文本选择 按下 v 或 V 可以进入可视化模式(visual mode),前者的文本选择是以字符为单位的,而后者的文本选择是以行为单位的。在可视化模式下,可以配合光标移动的一些操作来实现快速选择文本,例如按 V 进入行可视化模式后按 5j 即可快速选中当前行以及下面5行的内容。 查找替换 字符查找分为行内搜索和整个文件内的搜索: 按下 f 或 F 后输入单个字符在当前行内向右或向左查找首个匹配的字符 按下 / 后输入要查找的文本,然后按回车进入搜索模式,按 n 或 N 可以跳转到下一个或上一个匹配的文本处 有时候会需要在查找内容的同时将匹配的文本替换成其他值,可以在普通模式下通过以下命令完成: 输入 :s/old/new/,然后按回车,可以将当前行中的第一个 old 替换为 new 输入 :s/old/new/g,然后按回车,可以将当前行中的所有 old 替换为 new 输入 :%s/old/new/g,然后按回车,可以将整个文件中的所有 old 替换为 new 如果想要在匹配的项被替换前进行确认,在上述命令后加上 c 会对每一个匹配项的操作进行询问。 标记 可以使用标记 (mark) 功能来标记和跳转到文件中的某个位置,可以使用小写字母 a-z 或大写字母 A-Z 中的任意一个作为标记名称,按下 ma 则会在光标当前位置设置一个名称为 a 的标记,之后不管在当前文件的任何位置,只要按下 'a 即可跳转到标记 a 所在位置,按下 `a 则会跳转到标记所在行的首字符。 宏 有时候可能会需要在一个文档内重复执行一些相同的操作,利用 Vim 的宏就可以像封装一个函数一样,把一系列操作录制下来注册成一个宏,在需要时直接调用重放这些操作。使用宏的步骤如下: 在普通模式下,按 q 加一个字母开始录制。例如按下 qr,将该宏注册为 r 接下来的 Vim 内操作全部都会被记录,直到再次按下 q 的时候,宏就录制完成了 使用上面录制的宏,只需要在普通模式下按下 @ 加字母,例如按下 @r,就使用了一次宏 r 如果想多次使用宏,可以在 @ 前加上数字,例如按下 5@r,就会执行 5 次宏 r 如果想重复使用上一次的宏,可以在普通模式下按下 @@ ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/4/10
articleCard.readMore

「TIL」使用 Cloudflare Tunnel 穿透内网并增加登录保护

为了给朋友安利 Cloudflare Tunnel 以及协助他替换掉 frp 这款内网穿透应用,我花了几十分钟进行了学习和操作,将软路由上的 AList 通过 Cloudflare Tunnel 暴露到公网中,并增加了 OTP 验证为暴露出去的内网服务增加了一层安全保护。 Cloudflare Tunnel 准备工作 在开始前需要先拥有一个域名,然后将其托管到 Cloudflare 上,如果嫌麻烦可以直接在 Cloudflare 上注册域名。相关步骤这里略过不表。 创建 Tunnel 访问 Zero Trust 控制台,在侧边栏菜单找到 Access → Tunnels 并打开,点击页面上的 “Create a tunnel” 按钮创建一个新的通道,输入名称之后点击保存,然后会跳转到如下页面: Install Connector 安装连接器 在页面上半部分根据你的运行环境进行连接器版本的选择,如果这里没有对应的可选项,例如想要在 OpenWRT 中使用,可以在官方的 GitHub 仓库找到更多版本的连接器,我使用的是 R2S,所以选择的是 cloudflared-linux-arm64 这个版本。 在选择好后下载并安装连接器,然后根据页面中间部分的指引进行操作,这里需要复制一段带有 Token 的命令在命令行中执行,在操作完毕后,可以看到页面底部的 Connectors 中出现了一条已连接的记录。此时重新打开 Access → Tunnels 页面将会看到刚新增加的通道,它的状态应该是一个绿色的 HEALTHY,连接器已经正确地安装好了。 在 OpenWRT 中安装的指引 通过 SSH 进入到 OpenWRT 系统中,然后执行以下命令安装所选版本连接器,请注意替换链接和所下载的文件名称: curl -O -L https://github.com/cloudflare/cloudflared/releases/download/2023.3.1/cloudflared-linux-arm64 \ && chmod +x cloudflared-linux-arm64 \ && mv cloudflared-linux-arm64 /usr/bin/cloudflared 创建自启动: touch /etc/init.d/cloudflared && chmod +x /etc/init.d/cloudflared vim /etc/init.d/cloudflared 如果你的 OpenWRT 没有安装 vim 的话,就改成 nano 或者别的编辑器,然后将以下内容粘贴进去,其中 cfd_token 这个变量需要把上面截图红圈部分的 Token 复制过来填写进去: #!/bin/sh /etc/rc.common USE_PROCD=1 START=95 STOP=01 cfd_init="/etc/init.d/cloudflared" cfd_token="Your Token" boot() { ubus -t 30 wait_for network.interface network.loopback 2>/dev/null rc_procd start_service } start_service() { if [ $("${cfd_init}" enabled; printf "%u" ${?}) -eq 0 ] then procd_open_instance procd_set_param command /usr/bin/cloudflared --no-autoupdate tunnel run --token ${cfd_token} procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} procd_close_instance fi } stop_service() { pidof cloudflared && kill -SIGINT `pidof cloudflared` } 保存后执行以下命令启动连接器: /etc/init.d/cloudflared enable /etc/init.d/cloudflared start 公开服务 点击刚新增的通道名称,在弹出的抽屉页点击 Configure 进入配置页面,然后在 Public Hostname 这个 Tab 中点击按钮 “Add a public hostname” 创建一个公开的服务。 填写子域名 选择要使用的域名 选择本地服务的协议类型 本地服务的访问地址,一般为 192.168.x.y:8080 IP + 端口的形式 其他选项可以保持默认不动,保存后即可使用配置的域名访问内网的服务了。 访问认证 将内网服务直接对外暴露是一件很危险的事情,所幸 Cloudflare 提供了配套的认证服务,让我们可以简单几步就为暴露出去的内网服务增加访问认证功能。 在 Zero Trust 控制台打开 Settings → Authentication 页面,在 Login methods 中如果没有 One-time PIN 这个验证方式的话,点击 “Add new” 按钮后,选择 One-time PIN 即可添加 OTP 验证方式。 接下来在 Access → Applications 中点击按钮 “Add an application” 选择 Self-hosted 进入配置页面: 和前面的配置一样,1 和 2 分别是想要增加 OTP 验证的服务对应的域名,这里按需填写即可,如果想要对某个域名下的全部服务都开启,那么可以在 1 中填入通配符 *。认证服务选择 One-time PIN,其余选项保持默认即可,点击 “Next” 进入下一步,规则的配置。 规则配置 每一套配置方案有四种可选的动作,当规则命中时,将会执行方案配置中所选的动作: Policy Actions 规则有多种类型可以选择,可以根据邮件地址、国家、IP 地址等进行匹配,这里我选择使用邮箱后缀进行匹配,当访问者填写的邮箱是指定后缀的邮箱,则会命中规则,触发方案配置中的动作: Rules 规则保存后,可以再次使用配置的域名访问本地服务了,页面会需要填写邮箱接收 OTP 验证码: 当校验通过后,会跳转到本地服务,刷新页面后也不会要求重新认证,是 Cloudflare 帮我们做了背后的所有工作,赞美 Cloudflare! ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/4/4
articleCard.readMore

「TIL」在 Deta Space 中部署 Discord Bot

⚠️ discohook 在 2023-04-16 更新了版本,部分接口发生了变更。新版本已无需再手动拷贝添加命令 ID 到命令装饰器中,请悉知。 今天和朋友在 Discord 频道里聊天玩 ChatGPT 机器人,突然想要试试自己写一个 Discord Bot,因为最近一直在使用 Deta Space,并且之前也发现了一个可以在 Serverless 平台上使用的 Discord Bot 框架 discohook,所以尝试了一下在 Deta Space 上部署一个 Discord Bot 应用同时做一个简单的记录。 开发环境 首先创建一个 Deta 项目,在此之前需要先安装好 Space CLI,可以参考这篇文档安装并登录 ❯ mkdir discord-bot ❯ cd discord-bot ❯ space new ? What is your project's name? > discord-bot ~ No Spacefile found, trying to auto-detect configuration ... ~ Empty directory detected, creating discord-bot from scratch ... ✓ Project discord-bot created successfully! 操作后会在当前目录中自动创建一个空的 Spacefile 应用配置文件,这里可以暂时先放一放,后面再来进行配置。 然后需要创建 Python 的开发环境并且安装 discohook 这个包,我使用的是 PDM 这个包管理工具 ❯ pdm venv create ❯ pdm init ❯ pdm add git+https://github.com/jnsougata/discohook ❯ pdm add requests 至此,开发环境准备完毕。 功能开发 在 discohook 仓库中给出的示例代码有一个简单的 help 指令的实现 import discohook APPLICATION_ID = <YOUR_APPLICATION_ID> APPLICATION_TOKEN = <YOUR_APPLICATION_TOKEN> APPLICATION_PUBLIC_KEY = <YOUR_APPLICATION_PUBLIC_KEY> app = discohook.Client( application_id=APPLICATION_ID, token=APPLICATION_TOKEN, public_key=APPLICATION_PUBLIC_KEY ) @app.command( id="", # leave empty for now name="help", description="basic help command for the bot" ) async def help_command(interaction: discohook.Interaction): await interaction.response( "Hello, World!", embed=discohook.Embed(title="Help", description="This is a help command"), ephemeral=True, ) 在项目目录中创建一个 main.py 文件,把上面的代码拷贝进去,然后再做一些修改,把 Discord Bot 需要的 Token 等以环境变量的形式读取出来使用,然后再添加一个随机获取 meme 的命令 import os import discohook import requests APPLICATION_ID = os.environ.get('APPLICATION_ID') APPLICATION_TOKEN = os.environ.get('APPLICATION_TOKEN') APPLICATION_PUBLIC_KEY = os.environ.get('APPLICATION_PUBLIC_KEY') app = discohook.Client( application_id=APPLICATION_ID, token=APPLICATION_TOKEN, public_key=APPLICATION_PUBLIC_KEY ) @app.command( id="", name="help", description="basic help command for the bot" ) async def help_command(interaction: discohook.Interaction): await interaction.response( "Hello, World!", embed=discohook.Embed(title="Help", description="This is a help command"), ephemeral=True, ) @app.command( id="", name="meme", description="Send a random meme." ) async def meme(interaction: discohook.Interaction): resp = requests.get('https://meme-api.com/gimme') data = resp.json() meme_embed = discohook.Embed() meme_embed.image(data.get('url')) await interaction.response( data.get('title'), embed=meme_embed, ) 就这样,这个 Bot 就有两个可使用的命令了,不过距离真正可使用,还差一步,就是要先启动应用获取命令的 ID,把装饰器中的 id 补全,这样做的具体原因可以看仓库的这一段说明。 部署应用 在创建 Discord APP 和 Bot 以及获取命令 ID 前,需要先把应用部署到 Deta Space 上,然后获取应用的 URL 地址才能继续。 在部署前需要先在 Spacefile 中配置好应用的一些基础信息,更详细的说明可以参考官方文档 v: 0 icon: ./icon.png app_name: "DisTool Bot" micros: - name: discord-bot src: . engine: python3.8 presets: env: - name: APPLICATION_ID description: Your discord application id - name: APPLICATION_TOKEN description: Your discord application token - name: APPLICATION_PUBLIC_KEY description: Your discord application public key public_routes: - "/interactions" 通过 presets.env 预设了运行 Discord Bot 需要的三个环境变量,应用部署后可以在 Deta Space 应用设置界面中进行配置,然后将路径 /interactions 配置成公开的路由,Bot 可以直接通过这个路径进行交互。 配置完毕后还需要把依赖导出到 requirements.txt 中,在部署时才会被安装 ❯ pdm list --freeze -v > requirements.txt 完成上面的工作之后,就可以部署应用到 Deta Space 上了 ❯ space push ... ✓ Successfully pushed your code and updated your Builder instance! Run space release to create a release that others can install. Builder instance: https://xxxxxx.deta.app 部署成功后会看到一个实例的地址,这就是部署成功后的应用地址,剩下的就是跟着这里的说明一步步进行操作,获取到需要的三个环境变量,在 https://deta.space/ 中打开应用的设置界面,将对应的值填入保存即可。 配置环境变量 最后要做的就是访问 https://xxxxxx.deta.app/dh/dash/<bot_token_here> 打开控制台,复制各个命令的 ID 分别添加到对应命令装饰器的 id 中,然后再次执行 space push 重新部署即可。 添加 Bot 打开下面的链接(替换 client_id 为前面获取到的 APPLICATION_ID)后,即可邀请创建的 Bot 到频道中,然后在任意频道聊天框中输入 / 即可进行交互 https://discord.com/api/oauth2/authorize?client_id=<APPLICATION_ID>&permissions=274877959168&scope=bot / Command Bot Response 链接 Your first Space app 🚀 Space Docs Deta Space Developer Documentation and Guides https://deta.space/docs/en/introduction/first-app#installing-the-cli GitHub - jnsougata/discohook: discord bot framework for serverless applications discord bot framework for serverless applications. Contribute to jnsougata/discohook development by creating an account on GitHub. https://github.com/jnsougata/discohook GitHub - pdm-project/pdm: A modern Python package and dependency manager supporting the latest PEP standards A modern Python package and dependency manager supporting the latest PEP standards - GitHub - pdm-project/pdm: A modern Python package and dependency manager supporting the latest PEP standards https://github.com/pdm-project/pdm Discohook 0.0.5 documentation https://discohook.readthedocs.io/en/latest/index.html# ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/3/23
articleCard.readMore

Streamlit & Deta 开发体验

最近发现了Streamlit这个 Python 框架,粗略查看了其文档,发现其非常简单易用,上手很快。如果不需要开发自定义组件(Components),甚至不需要涉及任何与前端相关的开发内容。每个脚本文件都是一个页面,可以将其视为画布或容器。编写的 Python 代码从上到下执行,也就对应了页面上展示元素的顺序。由于已有许多封装好的基础组件和第三方组件,因此在使用时只需要几行简单的代码,再加上一些数据处理逻辑,就可以轻松地搭建出一个完整的页面。 在查看文档时,我注意到🔗连接数据源教程中出现了许多熟悉的名字。我选择了最感兴趣的 Deta Base 进行了研究。毕竟在开始使用 Deta 时,它还不叫 Deta Space 而是 Deta Cloud。当初看起来很小众的平台居然出现在其他工具的官方文档教程中,自然是非常好奇的。 这里稍微介绍一下 Deta Space 这个平台,它是一个云端的个人电脑,它是一个“个人云”。它的设计目的是让你能够在互联网上做更多的事情,同时让你掌控自己的数据。在 Deta Space 上,你可以发现和使用一些有趣和实用的应用,每个应用的用户都有自己的沙盒副本,包含了自己的微服务器、数据库和文件存储。 Streamlit 的官方定位是一个用于机器学习和数据可视化的开源 Python 框架。虽然我目前没有相关的开发需求,但是它能用几行代码构建出一个精美的在线应用,就它简单易用、开发快速和支持交互等优点而言,很难不让人想要创造需求以用它写点小玩具来练手。 在稍稍“创造需求”之后,最终决定以一个朋友之间内部使用的消息发布站点作为实验对象,使用 Streamlit + Deta Collection 进行简单实现。该站点的每一篇内容均包含标题、日期、内容和一张海报。正好可以使用 Deta Collection 的 Deta Base 和 Deta Drive 进行存储,前者存储文本内容,后者存储文件。另外,Deta 提供了 Python SDK,可以很方便地进行集成。 在使用 Streamlit 的过程中,可以实际感受到它的简单易用是真的很简单,而且代码量极少,部分组件的使用只需要一行代码就可以完成。例如,要在页面上放置一个文本输入框,只需要下面这一行简单的代码就可以了,而且在后续的逻辑中,可以直接通过 title 这个变量获取输入框的内容。 # 放置一个文本输入框 title = st.text_input('Title') Streamlit 还支持多页应用,每一个 Python 脚本文件,就相当于一个页面,唯一特殊的是主页面一般放置在根目录下(相当于首页),而其他页面则需要放置在 /pages 目录下,同时脚本文件的命名也有一定的规则需要遵循,如果感兴趣的话可以从🔗这里查看到更详细的内容。 开发完成后,如何部署应用呢?Streamlit 官方提供了一个开放且免费的平台,开发者可以在上面部署和分享自己的 Streamlit 应用。然而,经过一段时间的使用和观察,我发现 Streamlit Community Cloud 的服务好像不太稳定,偶尔会遇到服务掉线的情况,页面会一直显示加载状态。 关于 Deta Collection 的使用,好像没有太多需要说的。实际上,它非常简单易用。你只需要安装好 Deta SDK,然后在 Deta Space 中生成 Deta Collection 的 Token,在程序中就可以使用 Deta Base 和 Deta Drive 对数据和文件进行增删改查了。无论你是否了解 Deta,我都强烈建议你去尝试使用 Deta Space。 总的来说,使用 Streamlit 和 Deta 进行开发体验是非常快速且令人愉悦的。完善的官方文档和示例可帮助解决开发过程中的绝大部分问题。然而,Streamlit 是一个主要针对机器学习和数据可视化开发的框架,对于一些常见的非相关领域的 Web 开发来说确实有很多不足以应付的地方。因此,我个人不建议在一些复杂的 Web 应用开发中使用它。 链接 Deta Space The Cloud for Doers & Dreamers. https://deta.space/ Streamlit • The fastest way to build and share data apps Streamlit is an open-source app framework for Machine Learning and Data Science teams. Create beautiful web apps in minutes. https://streamlit.io/ ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/3/7
articleCard.readMore

在 Odoo 中集成可视化报表设计工具 ReportBro

💡 本文由 Notion AI & ChatGPT 辅助完成。 在 Odoo 中使用 QWeb 设计和开发报表可以满足大部分需求,但是当面临会频繁变更或者具有复杂布局的报表页面需求时,再使用 QWeb 就不是最佳的选择了。另外,如果可以有可视化的报表设计器来帮助我们设计并开发报表,编写代码的时间就大大缩短,并且可以节省许多开发成本,让我们更好地实现报表需求。 在最开始的时候,我在网上寻找了许多可视化报表设计工具,如 JasperReports, Seal Report 等,经过了深入研究及比较,最终决定了使用 ReportBro 这个基于 Web 端的可视化报表设计器,能够满足大部分需求,而且使用起来也非常的方便快捷,极大的提高了报表的设计效率。 ReportBro 是一款开源的可视化报表设计工具,可以用于生成复杂的 PDF 报表。它提供了一个可视化的界面,方便用户在不需要编程的情况下设计报表,支持自定义字体和各种常见的图形、元素,例如表格、图片、文本框、条形码等。此外,ReportBro 支持数据绑定,可以从数据源动态生成报表。并且它还配套提供了 Python 包 reportbro-lib 可以很方便地和任意的 Python 框架进行集成。通过与 Odoo 的集成,ReportBro 可以为业务提供更加高效和便捷的报表生成体验。 模块功能 通常情况下,报表与业务模型有着密切的联系,因此,在设计报表时,需要预先配置相应模型的报表模板变量,以便在生成报表输出时,可以根据业务数据进行动态填充。 下图为列表类型参数的配置,在列表参数的明细中可以使用 line 表示遍历出来的对象: 完成报表参数配置后,可在报表设计页面的左侧参数处查看我们定义的参数,在右侧的设计界面中拖拉相应元素进行布局,并选择数据源,以便轻松完成报表设计。 在报表设计的过程中,可以随时点击预览按钮 ▶️ 来查看报表效果,从而获得更加清晰的报表内容预览。此外,在预览报表的过程中,可以根据自己的实际情况,返回报表设计的布局视图对报表的内容进行编辑和调整,以确保最终得到的报表能够满足自身的需求。 ReportBro 默认提供了三种字体,均不支持中文。因此,需要使用自定义字体功能,将所需字体文件上传,并在设计报表时,为相应元素选择适当的中文字体,以确保渲染输出报表时无误。 为了保持一致的使用习惯,在报表设计完成后,还需要将报表的打印操作添加到相应模型表单的打印菜单中,以便用户可以直接在此处打印相关业务记录的报表。在报表模板变量配置页面中,可以一键添加或删除打印动作,快速完成该操作。 最终点击相应的打印动作,即可正常输出报表文件: ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2023/2/8
articleCard.readMore

Odoo16 中 X2Many 字段指定动态视图

大家应该都知道在 Odoo 的 context 中有若干用以指定视图的属性,如 form_view_ref 和 tree_view_ref 等,可以给字段指定其展示所使用的对应类型视图的。这个功能在 Odoo 16 之前的版本中,是可以指定一个动态的视图的,如根据单据类型的不同,在打开关系型字段的弹窗表单也可以随着单据类型变化。 <notebook> <page name="items" string="订单明细"> <field name="item_ids" context="{'form_view_ref': 'foo.' + order_type + '_order_item_view_form'}"/> </page> </notebook> 这个功能在 Odoo 16 中变得不太好使了,具体表现为只会使用当前记录首次解析出来的 form_view_ref 值,以上面的代码为例,若打开页面时,order_type 的值为 bar,则无论如何改变 order_type 的值,打开或创建 item_ids 对应的记录时打开的页面会一直是 foo.bar_order_item_view_form 对应的视图。 原因 通过 X2Many 字段组件的源码(/web/static/src/views/fields/x2many/x2many_field.js)可以发现创建和编辑的操作调用了 _openRecord(params) 这个方法,但是并没有发现任何跟视图相关的逻辑,继续往上一层找,可以看到调用了 useOpenX2ManyRecord() 这个方法,其中可以看到和视图有关的一段逻辑: /** * /web/static/src/views/fields/relational_utils.js */ const form = await getFormViewInfo({ list, activeField, viewService, userService, env }); 跳转到这个方法里面继续查看,在调用 loadViews() 加载视图的方法里将 context 作为参数传递了进去,到这里直觉告诉我离想要的答案不远了。 const { fields, relatedModels, views } = await viewService.loadViews({ context: list.context, resModel: comodel, views: [[false, "form"]], }); 接着打开 loadViews() 所在的源文件(/web/static/src/views/view_service.js)看看里面的实现,会发现下面这一段,把各种参数序列化成字符串后作为缓存的 Key 使用了,如果参数值没有变化,就不会通过 RPC 去请求获取视图的方法 get_views 了。 const { context, resModel, views } = params; const filteredContext = Object.fromEntries( Object.entries(context || {}).filter((k, v) => !String(k).startsWith("default_")) ); const key = JSON.stringify([resModel, views, filteredContext, loadViewsOptions]); if (!cache[key]) { ... } 在最开始我们提到过,context 中的 form_view_ref 值只会使用首次解析(实际上这里是 context 本身没有被重新解析)出来的值,因为 context 没有变化,其他参数也没有变化,通过上面的这段逻辑就可以知道,其实打开的视图都是缓存下来的。 很明显,问题就出在 context 没有重新解析这一点上,或者说,使用的 context 值是首次打开页面时解析的值,之后就不更新了。因此也很容易就可以定位到导致该问题的具体代码,就是 loadViews() 时传入的 list.context 有问题。 解决方案 再往回翻看一下 useOpenX2ManyRecord() 里的逻辑,可以看到 async function openRecord({ record, mode, context, title, onClose }) 这个方法的定义,是有传入 context 作为参数的,而在调用 getFormViewInfo() 时,却没有传递进去,而是直接通过 list 来获取 context 的值。 而在 Odoo 10 中,加载视图前会 var context = dataset.get_context(); 获取最新的 context 值,在 Odoo 14 中,则会使用通过参数传递进去的 context 值,这些 context 都是 load_views 前最新解析出来的值。所以在 Odoo 16 中,要修复这个问题,只需要让 getFormViewInfo() 这个方法可以用上最新的 context 值即可。 先来看看最终的实现: /** @odoo-module */ import { patch } from "@web/core/utils/patch"; import { X2ManyField } from "@web/views/fields/x2many/x2many_field"; patch(X2ManyField.prototype, 'x2many_field_with_dynamic_form_view_ref', { /** * x2m 字段打开前更新 list.context 中的视图引用值 * 使动态视图上下文可以生效 * * @override */ setup() { this._super(...arguments); // 保留原本的逻辑 const __openRecord = this._openRecord; this._openRecord = (params) => { // 仅当 context 中出现 form_view_ref 才更新 if (params.context && params.context.form_view_ref) { this.list.context.form_view_ref = params.context.form_view_ref; } // 执行原本的逻辑 __openRecord(params); }; } }); 非常简单的一个 patch 就可以解决指定动态视图的问题,只需要在 X2ManyField 组件的 setup() 初始化方法里将 _openRecord 的逻辑做一点变更,在它执行前把 list.context 更新成最新的值。这里只处理了 form_view_ref 这个属性,是因为想要保证在 patch 时只做最小化的修改,尽量不影响原本的行为。 ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2022/12/15
articleCard.readMore

Odoo 16 中新增的 Properties 字段

Odoo 16 正式发布已经有一段时间了,最近也在基于最新的版本做了一些开发,顺便学习了一下 Owl 并且开发了个小模块作为练习。在翻看 Project 这个应用的时候,发现在模型 project.task 上有一个 Properties 类型的字段,在之前的版本中是没有见到过的,于是翻阅了一下源码,在这里做个记录。 """ Field that contains a list of properties (aka "sub-field") based on a definition defined on a container. Properties are pseudo-fields, acting like Odoo fields but without being independently stored in database. This field allows a light customization based on a container record. Used for relationships such as <project.project> / <project.task>,... New properties can be created on the fly without changing the structure of the database. The "definition_record" define the field used to find the container of the current record. The container must have a :class:`~odoo.fields.PropertiesDefinition` field "definition_record_field" that contains the properties definition (type of each property, default value)... Only the value of each property is stored on the child. When we read the properties field, we read the definition on the container and merge it with the value of the child. That way the web client has access to the full field definition (property type, ...). """ 在 odoo/fields.py 中可以找到 Properties 类的定义,从注释里可以大致了解这个字段类型的一些基本信息: 适合轻度的自定义需求 字段包含了“容器”定义好的属性 这些属性表现和 Odoo 字段一样但是并不单独存储在数据库中(数据库表中不会有对应的列) 在一组有关联关系的模型中使用,其中一方为“容器”(如 project.project 和 porject.task 的关系) 需要配合“容器”中定义的 PropertiesDefinition 类型字段一起使用(存储了属性的定义) 属性的值会被存储在子记录中(和“容器”关联的记录,定义了 Properties 字段的模型的记录) 读取字段时会将“容器”的属性定义和子记录的字段值合并 实际看一下例子可能会更加容易理解这里面提到的一些概念,我们可以直接安装好项目模块,然后打开任意的任务记录,随意添加一些自定义属性试试。 上面截图中添加了三个属性,其中两个为 Char 一个为 Many2one 类型,分别来看一下属性的定义和存储的属性值是怎样的 # project.project 的 task_properties_definition 字段中存储的属性定义 [ { "default":"", "type":"char", "name":"93be0af4ab70ca68", "string":"Prop1" }, { "default":"", "type":"char", "name":"90bdf877fbbd91a7", "string":"Prop2" }, { "domain":false, "name":"a7e6b811e9401c30", "default":false, "comodel":"hr.employee", "type":"many2one", "string":"Prop3" } ] # project.task 的 task_properties 存储的属性值 {'90bdf877fbbd91a7': 'P2', '93be0af4ab70ca68': 'P1', 'a7e6b811e9401c30': 3} # 从 read 接口中读取到 project.task 的 task_properties 值 [ { "name": "93be0af4ab70ca68", "type": "char", "string": "Prop1", "default": "", "value": "P1" }, { "name": "90bdf877fbbd91a7", "type": "char", "string": "Prop2", "default": "", "value": "P2" }, { "name": "a7e6b811e9401c30", "type": "many2one", "domain": false, "string": "Prop3", "comodel": "hr.employee", "default": false, "value": [ 3, "Anita Oliver" ] } ] 从上面的数据结构可以看到属性的定义和属性值,是分别存储的,而实际在通过 ORM 方法获取的时候,Odoo 会将它们合并返回,这一点从源码中的 convert_to_record() 和 convert_to_read() 中可以看到。 在项目模块上使用这个类型的字段对任务添加自定义属性,让我联想到了 Jira 的任务管理,不同项目可以设置不同的任务属性,具有很强的可变性,不知道 Odoo 是不是有所参照? ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2022/11/27
articleCard.readMore

Odoo 下载 PDF 附件添加水印

这篇内容给大家简单讲讲怎么给 Odoo 中的 PDF 附件(ir.attachment)添加水印,本文相关代码均基于 Odoo 14 进行开发,在其他版本中可能会有所差异,请自行调试修改。 安装依赖 生成水印时会用到的库 reportlab 在 Odoo 14 的依赖中已经存在,如果你正确安装了依赖,则无需再次安装,如果你使用的是其他版本的 Odoo 或当前环境中没有找到该库,可以使用以下命令进行安装: pip install reportlab 功能实现 首先第一步当然是创建一个新的模块,这里把模块命名为 pdf_watermark,基本的目录结构如下: pdf_watermark ├── __init__.py ├── __manifest__.py └── models ├── __init__.py └── models.py Odoo 中附件的下载会经过 ir.http 的 def binary_content() 方法获取附件内容等必要信息,所以我们需要继承 ir.http 模型并重写 binary_content 方法,对 PDF 类型的附件添加水印,在 models.py 中添加继承的代码: import base64 import logging from odoo import models _logger = logging.getLogger(__name__) class IrHttp(models.AbstractModel): _inherit = 'ir.http' def binary_content(self, *args, **kwargs): status, headers, content = super(IrHttp, self).binary_content(*args, **kwargs) # 从请求头中获取附件类型,仅对 PDF 类型附件添加水印 content_type = dict(headers).get('Content-Type') if content_type == 'application/pdf': content = self.add_watermark(base64.b64decode(content)) content = base64.b64encode(content) return status, headers, content 上面的这段代码非常简单,就不多解释了,添加水印的核心在 self.add_watermark() 这句代码里,接下来我们就要实现这个方法。 在 def add_watermark() 这个方法中,大致会分成三个部分: 获取水印文本 生成水印文件 在 PDF 中添加水印(合并水印文件) 其中前面两部分的实现如下: from reportlab.pdfbase import pdfmetrics, ttfonts from reportlab.pdfgen import canvas from reportlab.lib.units import cm from odoo import fields, models class IrHttp(models.AbstractModel): _inherit = 'ir.http' def binary_content(self, *args, **kwargs): ... def _get_watermark(self): """ 获取水印文本 :return: """ return f'{self.env.company.name} {fields.Date.context_today(self)}' def _generate_watermark(self): """ 生成水印 :return: """ # 水印文件临时存储路径 filename = f'/tmp/watermark.pdf' watermark = self._get_watermark() # 获取画布并修改原点坐标 c = canvas.Canvas(filename) c.translate(1.5 * cm, -3 * cm) try: font_name = 'SimSun' # 从系统路径中引入中文字体(新宋) pdfmetrics.registerFont(ttfonts.TTFont('SimSun', 'SimSun.ttf')) except Exception as e: # 默认字体,不支持中文 font_name = 'Helvetica' _logger.error(f'Register Font Error: {e}') # 设置字体及大小,旋转 -20 度并设置颜色和透明度 c.setFont(font_name, 14) c.rotate(20) c.setFillColor('#27334C', 0.15) # 平铺写入水印 for i in range(0, 30, 6): for j in range(0, 35, 5): c.drawString(i * cm, j * cm, watermark) c.save() return filename 这里取了当前的公司名称及当前日期作为水印文本,你可以根据需要对 def _get_watermark() 这个方法进行修改,获取你所需的水印文本。 先看一下 def _generate_watermark() 这个方法里的 try...except... 语句,这里会尝试注册中文字体新宋,如果失败则会使用默认字体 Helvetica。 如果你的运行环境中没有对应的中文字体,但是水印中又出现中文的话,在输出的 PDF 文件里水印的中文会变成一个个方框,所以别忘了给运行环境安装可以使用的中文字体。 reportlab 会从系统路径中查找字体,以下是预定义的一些目录: TTFSearchPath = ( 'c:/winnt/fonts', 'c:/windows/fonts', '/usr/lib/X11/fonts/TrueType/', '/usr/share/fonts/truetype', '/usr/share/fonts', #Linux, Fedora '/usr/share/fonts/dejavu', #Linux, Fedora '%(REPORTLAB_DIR)s/fonts', #special '%(REPORTLAB_DIR)s/../fonts', #special '%(REPORTLAB_DIR)s/../../fonts',#special '%(CWD)s/fonts', #special '~/fonts', '~/.fonts', '%(XDG_DATA_HOME)s/fonts', '~/.local/share/fonts', #mac os X - from #http://developer.apple.com/technotes/tn/tn2024.html '~/Library/Fonts', '/Library/Fonts', '/Network/Library/Fonts', '/System/Library/Fonts', ) 各个系统字体的安装这里就略过不谈了,请自行查找相关内容。 继续回到水印的实现上来,剩下最后一步就是将生成的水印文件和将要下载的 PDF 文件合并在一起,来看看具体实现: import io from PyPDF2 import PdfFileWriter, PdfFileReader class IrHttp(models.AbstractModel): _inherit = 'ir.http' def binary_content(self, *args, **kwargs): ... def _get_watermark(self): ... def _generate_watermark(self): ... def add_watermark(self, content): """ 添加水印 :param content: :return: """ watermark = self._generate_watermark() pdf_input = PdfFileReader(io.BytesIO(content), strict=False) watermark = PdfFileReader(open(watermark, "rb"), strict=False) pdf_output = PdfFileWriter() page_count = pdf_input.getNumPages() for page_number in range(page_count): input_page = pdf_input.getPage(page_number) input_page.mergePage(watermark.getPage(0)) pdf_output.addPage(input_page) stream = io.BytesIO() pdf_output.write(stream) data = stream.getvalue() return data 合并步骤很简单,就是遍历要下载的 PDF 将它的每一页都和水印文件合并起来,然后再输出即可。 上面就是全部的实现了,安装模块之后,可以在开启开发者模式后打开「设置——技术——附件」菜单,创建并上传一个 PDF 文件,然后再下载查看是否成功添加上了水印。 如果你想要直接使用这个模块或者下载完整的源码,可以前往这里查看或下载;如果你想查看完整的 commits 信息,可以查看这个 PR 里的内容。 扩展阅读 How to watermark your PDF files with Python. One of the most ancient yet efficient ways to protect your PDF work is by watermarking. In the past, we had to manually stamp it with a watermark when sending a PDF by e-mail to customers or relations. In the following bullet points I will show you how we automated this in a simple, yet efficient manner! https://medium.com/@schedulepython/how-to-watermark-your-pdf-files-with-python-f193fb26656e How to set any font in reportlab Canvas in python? I'm using reportlab to create pdfs. When I try to set a font using the following method, I get a KeyError: pdf = Canvas('test.pdf') pdf.setFont('Tahoma', 16) But if I use 'Courier' instead of 'Ta... https://stackoverflow.com/questions/4899885/how-to-set-any-font-in-reportlab-canvas-in-python ©️  本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

2020/12/27
articleCard.readMore