高级主题和示例
调度程序
Py4web 有一个内置的调度程序。您无需安装或配置任何东西就能使其工作。
给定一个任务(只是一个 python 函数),您可以安排该函数的异步运行。运行可以是一次性的,也可以是周期性的。他们可以因超时而停止。它们可以被安排在给定的预定时间运行。
调度器通过创建表 task_run 并将预定义任务的运行作为表记录排队来工作。每个 task_run 都引用一个任务,并包含要传递给该任务的输入。调度程序将在 db.task_run.log 中保存捕获到的任务的 stdout + stderr,并在 db.task_run.output 中保存捕获到的任务输出。
py4web 线程循环并找到下一个需要执行的任务。对于每个任务,它都会创建一个工作进程,并将任务分配给该工作进程。您可以指定应同时运行多少个工作进程。工作进程是守护进程,它们只在一个任务运行的生命周期内存在。每个工作进程只负责单独执行一个任务。主循环负责分配任务和超时。
该系统非常强大,因为唯一的真实来源是数据库,其完整性由事务安全保证。即使 py4web 被终止,正在运行的任务也会继续运行,除非它们完成、失败或被明确终止。
除了允许在一个节点上执行多个并发任务外,还可以在不同的计算节点上运行调度器的多个实例,只要它们对 task_run 使用相同的客户端/服务器数据库,并且它们都定义了相同的任务。
以下是一个如何使用调度器的示例:
from pydal.tools.scheduler import Scheduler, delta, now
from .common import db
# create and start the scheduler
scheduler = Scheduler(db, sleep_time=1, max_concurrent_runs=1)
scheduler.start()
# register your tasks
scheduler.register_task("hello", lambda **inputs: print("hi!"))
scheduler.register_task("slow", lambda: time.sleep(10))
scheduler.register_task("periodic", lambda **inputs: print("I am periodic!"))
scheduler.register_task("fail", lambda x: 1 / x)
# enqueue some task runs:
scheduler.enqueue_run(name="hello")
scheduler.enqueue_run(name="hello", scheduled_for=now() + delta(10) # start in 10 secs
scheduler.enqueue_run(name="slow", timeout=1) # 1 secs
scheduler.enqueue_run(name="periodic", period=10) # 10 secs
scheduler.enqueue_run(name="fail", inputs={"x": 0})
请注意,在 scaffolding 应用程序中,如果 settings.py 中的 USE_SCHEDULER=True ,则会创建并启动调度器。
您可以通过后台管理应用程序或使用 Grid(db.task_run) 来管理任务运行。
为了防止数据库锁(特别是 sqlite),我们建议:
为调度程序和其他所有内容使用不同的数据库
每次插入/更新/删除后,都要尽快执行
db.commit()尝试将任务中的数据库逻辑打包到
try...except语句块中,如下:
def my_task():
try:
# do something
db.commit()
except Exception:
db.rollback()
使用后台任务发送消息
作为上述应用的一个示例,考虑希望从后台任务异步发送电子邮件的情况。在这个例子中,我们使用 Twilio 的 SendGrid 发送它们 (https://www.twilio.com/docs/sendgrid/for-developers/sending-email/quickstart-python) 。
以下是发送电子邮件的可能调度任务:
import sendgrid
from sendgrid.helpers.mail import Mail, Email, To, Content
def sendmail_task(from_addr, to_addrs, subject, body):
""
# build the messages using sendgrid API
from_email = Email(from_addr) # Must be your verified sender
content_type = "text/plain" if body[:6] != "<html>" else "text/html"
content = Content(content_type, body)
mail = Mail(from_email, To(to_addrs), subject, content)
# ask sendgrid to deliver it
sg = sendgrid.SendGridAPIClient(api_key=settings.SENDGRID_API_KEY)
response = sg.client.mail.send.post(request_body=mail.get())
# check if worked
assert response.status_code == "200"
# register the above task with the scheduler
scheduler.register_task("sendmail", sendmail_task)
要安排发送新电子邮件,请执行以下操作:
email = {
"from_addr": "me@example.com",
"to_addrs": ["me@example.com"],
"subject": "Hello World",
"body": "I am alive!",
}
scheduler.enqueue_run(name="sendmail", inputs=email, scheduled_for=None)
电子邮件表示中的 key:value 必须与任务的参数匹配。 scheduled_for 参数是可选的,允许您指定何时发送电子邮件。您可以使 Dashboard 查看名为 sendmail 的任务的 task_run 状态。
您还可以告诉 auth 利用上述机制发送电子邮件:
class MySendGridSender:
def __init__(self, from_addr):
self.from_addr = from_adds
def send(self, to_addr, subject, body):
email = {
"from_addr": self.from_addr,
"to_addrs": [to_addr],
"subject": subject,
"body": body,
}
scheduler.enqueue_run(name="sendmail", inputs=email)
auth.sender = MySendGridSender(from_addr="me@example.com")
您也可以告诉 auth 访问上述机制。通过上述操作,auth 将不会使用 smtplib 发送电子邮件。相反,它将使用调度程序通过 SendGrid 发送它们。请注意,这里唯一的要求是 auth.sender 必须是一个具有与示例中相同签名的 send 法的对象。
请注意,也可以发送短信而不是电子邮件,但这需要 1)将电话号码存储在 auth_user 中,2)覆盖发送电子邮件的 Auth.send 方法:
Celery
对。您可以使用 Celery 来代替内置调度程序,但它增加了复杂性,而且不那么健壮。然而,内置调度器是为长时间运行的任务而设计的,如果您有数百个任务同时运行,数据库可能会成为瓶颈。如果您有 100 多个并发任务,而且它们是短时间运行的任务,Celery 可能会工作地更好。
py4web 和 asyncio
Asyncio 不是严格需要的,至少在大多数正常用例中,由于其并发模型,它会增加问题而不是价值。另一方面,我们认为 py4web 需要一个内置的基于 websocket 异步的解决方案。
如果您打算使用 asyncio ,请注意您还应该处理框架的所有组件:特别是 pydal 不符合 asyncio ,因为并非所有适配器都支持 async。
htmx
目前有许多 javascript 前端框架可供选择 , 它们使您在设计 web 客户端方面具有极大的灵活性。Vue 、React 和 Angular 只是其中的几个。然而,构建这些系统之一的复杂性阻碍了许多开发人员获得这些好处。再加上生态系统的快速变化,你很快就会拥有一个在一两年后难以维护的应用程序。
因此,越来越需要使用简单的 html 元素来增加网页的交互性。htmx 是在没有 javascript 复杂性的情况下成为页面交互性领导者的工具之一。从技术上讲,htmx 允许您使用属性直接在 HTML 中访问 AJAX 、CSS 转换、Web 套接字和服务器发送事件,因此您可以使用简单性且强大功能的超文本构建现代用户界面。 [CIT1601]
在官方网站上阅读有关 htmx 及其功能的所有信息,网址为 https://htmx.org 。如果你愿意,还有一个视频教程: Simple, Fast Frontends With htmx 。
py4web 通过多种方式支持 htmx 集成。
允许您将 htmx 属性添加到表单和按钮中
包含一个用于 py4web 网格的 htmx 属性插件
表单中 htmx 的用法
py4web Form 类允许您将 **kwargs 传递给它,这些 kwargs 将作为属性传递给 html 表单。例如,要将 hx-post 和 hx-target 添加到 <form> 元素中,您将使用:
attrs = {
"_hx-post": URL("url_to_post_to/%s" % record_id),
"_hx-target": "#detail-target",
}
form = Form(
db.tablename,
record=record_id,
**attrs,
)
现在,当您的表单被提交时,它将调用 hx-post 属性中的 URL,返回给浏览器的任何内容都将用来替换 id 是 “detail-target” 的元素内的 html。
让我们继续一个完整的例子(从 scaffold 开始)。
controllers.py
import datetime
@action("htmx_form_demo", method=["GET", "POST"])
@action.uses("htmx_form_demo.html")
def htmx_form_demo():
return dict(timestamp=datetime.datetime.now())
@action("htmx_list", method=["GET", "POST"])
@action.uses("htmx_list.html", db)
def htmx_list():
superheros = db(db.superhero.id > 0).select()
return dict(superheros=superheros)
@action("htmx_form/<record_id>", method=["GET", "POST"])
@action.uses("htmx_form.html", db)
def htmx_form(record_id=None):
attrs = {
"_hx-post": URL("htmx_form/%s" % record_id),
"_hx-target": "#htmx-form-demo",
}
form = Form(db.superhero, record=db.superhero(record_id), **attrs)
if form.accepted:
redirect(URL("htmx_list"))
cancel_attrs = {
"_hx-get": URL("htmx_list"),
"_hx-target": "#htmx-form-demo",
}
form.param.sidecar.append(A("Cancel", **cancel_attrs))
return dict(form=form)
templates/htmx_form_demo.html
[[extend 'layout.html']]
[[=timestamp]]
<div id="htmx-form-demo">
<div hx-get="[[=URL('htmx_list')]]" hx-trigger="load" hx-target="#htmx-form-demo"></div>
</div>
<script src="https://unpkg.com/htmx.org@1.3.2"></script>
templates/htmx_list.html
<ul>
[[for sh in superheros:]]
<li><a hx-get="[[=URL('htmx_form/%s' % sh.id)]]" hx-target="#htmx-form-demo">[[=sh.name]]</a></li>
[[pass]]
</ul>
templates/htmx_form.html
[[=form]]
我们现在有一个功能强大的维护应用程序来更新我们的 superheros 。在浏览器中导航到新应用程序中的 htmx_form_demo 页面。在 htmx_form_demo.html 页面内的 div 中,hx-trigger="load" 属性会在 htmx_form_demo 页面加载完成后,将 htmx_list.html 页面加载到 htmx-form-demo 这个 DIV 元素中。
请注意,在 htmx-form-demo 这个 DIV 元素之外添加的时间戳在页面进行过渡(交互)时不会改变。这是因为外部页面永远不会重新加载,只会重新加载 htmx-form-demo 这个 DIV 元素中的内容。
然后,在锚点标签上使用 htmx 的属性 hx-get 和 hx-target 来调用 htmx_form 页面,以将表单加载到 htmx-form-demo 这个 DIV 元素中。
到目前为止,我们刚刚看到了标准的 htmx 处理。这里没有什么花哨的东西,也没有什么特定于 py4web 的东西。然而,在 htmx_form 方法中,我们看到了如何将任何属性传递给 py4web 表单,当我们添加 hx-post 和 hx-target 时,该表单将在 <form> 元素上呈现。这告诉表单允许 htmx 覆盖默认表单行为,并在指定的目标中呈现结果输出。
默认的 py4web 表单不包含取消按钮,以防您想取消编辑表单。但您可以在表单中添加 “sidecar” 元素。您可以在 htmx_form 中看到,我们添加了一个 cancel 选项并添加了所需的 htmx 属性,以确保 htmx_list 页面呈现在 htmx-form-demo DIV 中。
在 Grid 中使用 htmlx
py4web 的 grid 提供了一个属性插件系统,允许您构建插件,为表单元素、锚元素或确认消息提供自定义属性。py4web 还提供了一个专门针对 htmx 的属性插件。
这是一个基于前面的 htmx 表单示例构建的示例。
controller.py
@action("htmx_form/<record_id>", method=["GET", "POST"])
@action.uses("htmx_form.html", db)
def htmx_form(record_id=None):
attrs = {
"_hx-post": URL("htmx_form/%s" % record_id),
"_hx-target": "#htmx-form-demo",
}
form = Form(db.superhero, record=db.superhero(record_id), **attrs)
if form.accepted:
redirect(URL("htmx_list"))
cancel_attrs = {
"_hx-get": URL("htmx_list"),
"_hx-target": "#htmx-form-demo",
}
form.param.sidecar.append(A("Cancel", **cancel_attrs))
return dict(form=form)
@action("htmx_grid")
@action.uses( "htmx_grid.html", session, db)
def htmx_grid():
grid = Grid(db.superhero, auto_process=False)
grid.attributes_plugin = AttributesPluginHtmx("#htmx-grid-demo")
attrs = {
"_hx-get": URL(
"htmx_grid",
),
"_hx-target": "#htmx-grid-demo",
}
grid.param.new_sidecar = A("Cancel", **attrs)
grid.param.edit_sidecar = A("Cancel", **attrs)
grid.process()
return dict(grid=grid)
templates/htmx_form_demo.html
[[extend 'layout.html']]
[[=timestamp]]
<div id="htmx-form-demo">
<div hx-get="[[=URL('htmx_list')]]" hx-trigger="load" hx-target="#htmx-form-demo"></div>
</div>
<div id="htmx-grid-demo">
<div hx-get="[[=URL('htmx_grid')]]" hx-trigger="load" hx-target="#htmx-grid-demo"></div>
</div>
<script src="https://unpkg.com/htmx.org@1.3.2"></script>
注意,我们添加了 id 是 htmx-grid-demo 的 DIV ,其内部调用了 htmx_grid 路由。
templates/htmx_grid.html
[[=grid.render()]]
在 htmx_grid 中,我们利用了 grid 上的延迟处理。我们设置了一个标准的 CRUD grid,延迟处理,然后告诉 grid 我们将使用一个替代属性插件来构建我们的导航。现在表单、链接和删除确认都由 htmx 处理。
使用 htmx 的自动完成小部件
htmx 不仅可以用于 form/grid 格处理。在这个例子中,我们将利用 htmx 和 py4web 表单小部件来构建一个可以在表单中使用的自动补全小部件。 注意:这只是一个示例,py4web 中没有此代码
我们将再次使用示例应用程序中定义的 superheros 数据库。
将以下内容添加到你的 controllers.py 中。这段代码将构建你的自动完成下拉菜单,并处理数据库调用以获取数据。
import json
from functools import reduce
from yatl import DIV, INPUT, SCRIPT
from py4web import action, request, URL
from ..common import session, db, auth
@action(
"htmx/autocomplete",
method=["GET", "POST"],
)
@action.uses(
"htmx/autocomplete.html",
session,
db,
auth.user,
)
def autocomplete():
tablename = request.params.tablename
fieldname = request.params.fieldname
autocomplete_query = request.params.query
field = db[tablename][fieldname]
data = []
fk_table = None
if field and field.requires:
fk_table = field.requires.ktable
fk_field = field.requires.kfield
queries = []
if "_autocomplete_search_fields" in dir(field):
for sf in field._autocomplete_search_fields:
queries.append(
db[fk_table][sf].contains(
request.params[f"{tablename}_{fieldname}_search"]
)
)
query = reduce(lambda a, b: (a | b), queries)
else:
for f in db[fk_table]:
if f.type in ["string", "text"]:
queries.append(
db[fk_table][f.name].contains(
request.params[f"{tablename}_{fieldname}_search"]
)
)
query = reduce(lambda a, b: (a | b), queries)
if len(queries) == 0:
queries = [db[fk_table].id > 0]
query = reduce(lambda a, b: (a & b), queries)
if autocomplete_query:
query = reduce(lambda a, b: (a & b), [autocomplete_query, query])
data = db(query).select(orderby=field.requires.orderby)
return dict(
data=data,
tablename=tablename,
fieldname=fieldname,
fk_table=fk_table,
data_label=field.requires.label,
)
class HtmxAutocompleteWidget:
def __init__(self, simple_query=None, url=None, **attrs):
self.query = simple_query
self.url = url if url else URL("htmx/autocomplete")
self.attrs = attrs
self.attrs.pop("simple_query", None)
self.attrs.pop("url", None)
def make(self, field, value, error, title, placeholder="", readonly=False):
# TODO: handle readonly parameter
control = DIV()
if "_table" in dir(field):
tablename = field._table
else:
tablename = "no_table"
# build the div-hidden input field to hold the value
hidden_input = INPUT(
_type="text",
_id="%s_%s" % (tablename, field.name),
_name=field.name,
_value=value,
)
hidden_div = DIV(hidden_input, _style="display: none;")
control.append(hidden_div)
# build the input field to accept the text
# set the htmx attributes
values = {
"tablename": str(tablename),
"fieldname": field.name,
"query": str(self.query) if self.query else "",
**self.attrs,
}
attrs = {
"_hx-post": self.url,
"_hx-trigger": "keyup changed delay:500ms",
"_hx-target": "#%s_%s_autocomplete_results" % (tablename, field.name),
"_hx-indicator": ".htmx-indicator",
"_hx-vals": json.dumps(values),
}
search_value = None
if value and field.requires:
row = (
db(db[field.requires.ktable][field.requires.kfield] == value)
.select()
.first()
)
if row:
search_value = field.requires.label % row
control.append(
INPUT(
_type="text",
_id="%s_%s_search" % (tablename, field.name),
_name="%s_%s_search" % (tablename, field.name),
_value=search_value,
_class="input",
_placeholder=placeholder if placeholder and placeholder != "" else "..",
_title=title,
_autocomplete="off",
**attrs,
)
)
control.append(DIV(_id="%s_%s_autocomplete_results" % (tablename, field.name)))
control.append(
SCRIPT(
"""
htmx.onLoad(function(elt) {
document.querySelector('#%(table)s_%(field)s_search').onkeydown = check_%(table)s_%(field)s_down_key;
\n
function check_%(table)s_%(field)s_down_key(e) {
if (e.keyCode == '40') {
document.querySelector('#%(table)s_%(field)s_autocomplete').focus();
document.querySelector('#%(table)s_%(field)s_autocomplete').selectedIndex = 0;
}
}
})
"""
% {
"table": tablename,
"field": field.name,
}
)
)
return control
用法 - 在控制器代码中,此示例使用 bulma 作为基本的 css 格式化程序。
formstyle = FormStyleFactory()
formstyle.classes = FormStyleBulma.classes
formstyle.class_inner_exceptions = FormStyleBulma.class_inner_exceptions
formstyle.widgets["vendor"] = HtmxAutocompleteWidget(
simple_query=(db.vendor.vendor_type == "S")
)
form = Form(
db.product,
record=product_record, # defined earlier in controller
formstyle=formstyle,
)
首先,获取 FormStyleFactory 的实例。然后从您想要的任何 css 框架中获取基本的 css 类。在 css 框架中添加类内部异常。设置好后,您可以根据字段的名称覆盖字段的默认小部件。在这种情况下,我们将覆盖 “vendor” 字段的小部件。我们没有将所有 vendors 都包含在选择下拉列表中,而是仅限于其类型等于 “S” 的供应商。
当这在您的页面中呈现时,vendor 字段的默认小部件将被 HtmxAutocompleteWidget 生成的小部件替换。当您向 HtmxAutocompleteWidget 传递一个简单的查询时,该小部件将通过默认路由使用数据填充下拉列表。
如果使用简单查询和默认构建的 url ,则仅限于简单的 DAL 查询。您不能在此简单查询中使用 DAL 子查询。如果下拉列表的数据需要更复杂的 DAL 查询,您可以覆盖默认的数据构建器 URL ,以提供自己的控制器函数来检索数据。
来自 https://htmx.org 网站
utils.js
在本文档中,我们多次提到 scaffolding 应用程序附带的 utils.js,但我们从未明确列出其中的内容。所以,在这里说明。
string.format
它扩展了 String 对象原型,允许这样的表达式:
var a = "hello {name}".format(name="Max");
Q 对象
Q 对象可以像支持 jQuery 语法的选择器一样使用:
var element = Q("#element-id")[0];
var selected_elements = Q(".element-class");
它支持与 JS querySelectorAll 相同的语法,并且始终返回选定元素的数组(可以为空)。
Q 对象也是函数的容器,在 Javascript 编程时非常有用。它是无状态的
例如:
Q.clone
克隆任何对象的函数:
var b = {any: "object"}
var a = Q.clone(b);
Q.eval
它计算字符串中的 JS 表达式。它不是沙盒。
var a = Q.eval("2+3+Math.random()");
Q.ajax
JS fetch 方法的包装器,提供了更好的语法:
var data = {};
var headers = {'custom-header-name': 'value'}
var success = response => { console.log("recereived", response); }
var failure = response => { console.log("recereived", response); }
Q.ajax("POST", url, data, headers).then(success, failure);
Q.get_cookie
从当前页面的 cookie 头部按名称提取 cookie:如果 cookie 不存在,则返回 null。可以在页面的 JS 中使用以检索会话 cookie ,以备调用 cookie 的 API。
var a = Q.get_cookie("session");
Q.register_vue_component
这是 Vue 2 特有的,将来可能会被弃用,但它允许定义一个 Vue 组件,其中模板存储在单独的 HTML 文件中,并且只有在使用该组件时才会延迟加载模板。
例如,与其这样做:
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
});
您可以将模板放入 button-counter.html 中,然后执行以下操作
Q.register_vue_component("button-counter", "button-counter.html", function(res) {
return {
data: function () {
return {
count: 0
};
};
});
Q.upload_helper
它允许将 file 类型的输入标签绑定到回调,以便在选择文件时加载所选文件的内容,进行 base64 编码,并传递给回调。
这对于创建包含输入字段选择器的表单很有用,但您希望将所选文件的内容放入变量中,例如对该内容进行 ajax post。
例如:
<input type="file" id="my-id" />
and
var file_name = ""
var file_content = "";
Q.upload_helper("my_id", function(name, content) {
file_name = name;
file_content = content; // base 64 encoded;
}
T 对象
这是 Python 中 pluralize 库的 Javascript 重新实现,该库由 py4web 中的 Python T 对象使用。因此,它本质上是一个客户端(浏览器端)的 T 对象。
T.translations = {'dog': {0: 'no cane', 1: 'un case', 2: '{n} cani', 10: 'tanti cani'}};
var message = T('dog').format({n: 5}); // "5 cani"
预期用途是创建一个服务器端点,该端点可以为客户端接受的语言提供翻译,通过 ajax-get 获得 T 翻译,然后使用 T 在客户端而不是服务器端翻译和多元化所有消息。
Q.debounce
防止函数自身干扰(重复执行冲突)。
setInterval(500, Q.debounce(function(){console.log("hello!")}, 200);
并且该函数将每 500 ms 调用一次,但如果前一次调用没有终止,则将跳过。与其他去盎司实现不同,它通过延迟来确保最后一个调用始终被执行(在示例中为 200 ms)
Q.debounce
防止函数被过于频繁地被调用;
Q("#element").onclick = Q.debounce(function(){console.log("clicked!")}, 1000);
如果元素的点击频率超过每 1000ms 一次,则其他点击将被忽略。
Q.tags_inputs
它将包含以逗分为分隔标签的字符串的常规文本输入转换为标签小部件。例如:
<input name="browsers"/>
在 JSL 中
Q.tags_input('[name=zip_codes]')
您可以通过以下方式限制选项集:
Q.tags_input('[name=zip_codes]', {
freetext: false,
tags: ['Chrome', 'Firefox', 'Safari', 'Edge']
});
与 datalist 元素配合使用以提供自动补全功能。只需在数据列表 id 前添加 -list :"
<input name="browsers"/>
<datalist id="browses-list">
<option>Chrome</option>
<option>Firfox</option>
<option>Safari</option>
<option>Edge</option>
</datalist>
在 JS 中
Q.tags_input('[name=zip_codes]', {freetext: false});
它提供了更多未记录的选项。你需要设计标签的样式。例如:
ul.tags-list {
padding-left: 0;
}
ul.tags-list li {
display: inline-block;
border-radius: 100px;
background-color: #111111;
color: white;
padding: 0.3em 0.8em 0.2em 0.8em;
line-height: 1.2em;
margin: 2px;
cursor: pointer;
text-transform: capitalize;
}
ul.tags-list li[data-selected=true] {
opacity: 1.0;
}
注意,如果输入元素具有 .type-list-string 或 .type-list-integer ,utils.js 会自动应用 tag_input 函数。
Q.score_input
Q.score_input(Q('input[type=password]')[0]);
这将把密码输入变成一个对密码复杂性进行评分的小部件。它会自动应用于名为 “password” 或 “new_password” 的 input 组件。
组件
这是一个简易版的 HTMX。它允许在页面中插入通过 AJAX 加载的 ajax-component 标签,且这些组件中的任何表单都会被拦截(即表单提交的结果也会显示在同一个组件内)。
例如,想象一个 index.html 包含
<ajax-component id="component_1" url="[[=URL('mycomponent')]]">
<blink>Loading...</blink>
</ajax-component>
还有一个为组件服务的不同 action:
@action("mycomponent", method=["GET", "POST"])
@action.uses(flash)
def mycomponent():
flash.set("Welcome")
form = Form([Field("your_name")])
return DIV(
"Hello " + request.forms["your_name"]
if form.accepted else form).xml()
组件 action 是一个常规 action ,除了它应该生成没有 <html><body>...</body></html> 封装的 html,例如,它可以使用模板和 flash 等。
请注意,如果主页支持 flash 消息,则组件中的任何 flash 消息都将由父页面显示。
此外,如果组件返回 redirect("other_page") ,则不仅是组件的内容,整个页面都将被重定向。
组件 html 的内容可以包含 <script> ... </script> ,它们可以修改全局页面变量以及修改其他组件。
使用 Altcha 添加验证码的解决方案
本节使用 Altcha 库为 py4web 应用程序提供了一个简单的验证码实现。虽然没有经过详尽的测试,但它可以作为集成强大的客户端验证码解决方案的一个实例。更多信息请访问 https://altcha.org
先决条件
首先,您需要安装 Altcha 库。你可以使用 pip 来实现这一点:
python3 -m pip install --upgrade altcha
您还需要一个密钥来进行 HMAC 验证。建议将其存储在应用程序的设置中。对于这个例子,我们假设你有一个像 .settings.py 这样的文件,其中包含以下变量:
# .settings.py
ALTCHA_HMAC_KEY = "your-very-secret-key-here"
控制器逻辑
接下来,您需要将必要的 actions 添加到控制器文件中。以下代码提供了两个 actions :一个用于生成验证码挑战( altcha ),另一个用于处理包含该验证码的表单( some_form )。
# controllers/default.py
from altcha import (
create_challenge,
verify_solution,
ChallengeOptions,
)
from py4web import action, response, request, URL, Field, flash, Form
from py4web.utils.form import XML, T
from .settings import ALTCHA_HMAC_KEY
@action("altcha", method=["GET"])
def get_altcha():
"""Generates and returns an Altcha challenge."""
try:
challenge = create_challenge(
ChallengeOptions(
hmac_key=ALTCHA_HMAC_KEY,
max_number=50000,
)
)
response.headers["Content-Type"] = "application/json"
return challenge.__dict__
except Exception as e:
response.status = 500
return {"error": f"Failed to create challenge: {str(e)}"}
@action.uses("form_altcha.html", session, flash)
def some_form():
"""An example form that uses the Altcha captcha."""
fields = [
Field("name", requires=IS_NOT_EMPTY()),
Field("color", type="string", requires=IS_IN_SET(["red", "blue", "green"])),
]
form = Form(fields,
csrf_session=session,
submit_button=T("Submit"))
# Insert the Altcha widget HTML before the submit button
form.structure.insert(-1, XML('<altcha-widget></altcha-widget></br>'))
if form.accepted:
altcha_payload = request.POST.get("altcha")
if not altcha_payload:
response.status = 400
flash.set("NO ALTCHA payload")
print("NO ALTCHA payload")
else:
ok, error = verify_solution(altcha_payload, ALTCHA_HMAC_KEY)
if not ok:
response.status = 400
flash.set(f"ALTCHA verification fail: {error}")
print("ALTCHA verification fail:", error)
else:
flash.set("ALTCHA verified.")
return dict(form=form)
视图模板
您需要包含 Altcha 的 JavaScript 库,并在 HTML 模板中配置小部件。
form_altcha.html
此模板与 some_form action 配合使用。它加载 Altcha 脚本并设置 challengeurl 属性以指向我们的 altcha action 。
[[extend 'layout.html']]
<script async defer src="https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js" type="module"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const altchaWidget = document.querySelector("altcha-widget");
if (altchaWidget) {
altchaWidget.setAttribute("challengeurl", "[[=URL('altcha')]]");
}
});
</script>
<div class="section">
<div class="vars">[[=form]]</div>
</div>
自定义 Auth Form
对于自定义身份验证表单,您可以采用类似的方法。确保将 <altcha-widget> 标签插入表单的结构中,并包含必要的 JavaScript。
[[extend "layout.html"]]
<script async defer src="https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js" type="module"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const altchaWidget = document.querySelector("altcha-widget");
if (altchaWidget) {
altchaWidget.setAttribute("challengeurl", "[[=URL('altcha')]]");
}
});
</script>
<style>
.auth-container {
max-width: 80%;
min-width: 400px;
margin-left: auto;
margin-right: auto;
border: 1px solid #e1e1e1;
border-radius: 10px;
padding: 20px;
}
</style>
[[form.structure.insert(-1,XML('<altcha-widget></altcha-widget></br>'))]]
<div class="auth-container">[[=form]]</div>
要在 auth 表单中启用 Altcha ,您可以使用以下夹具:
class AltchaServerFixture(Fixture):
def __init__(self, hmac_key=ALTCHA_HMAC_KEY):
super().__init__()
self._name = "altcha_server"
self.hmac_key = hmac_key
def on_success(self, context):
# Only verify Altcha for POST requests
if request.method != "POST":
return
payload = request.POST.get("altcha")
if not payload:
raise HTTP(400, "ALTCHA payload not received")
try:
verified, err = verify_solution(payload, self.hmac_key, True)
if not verified:
raise HTTP(400, "Invalid ALTCHA")
return {"success": True, "message": "Altcha verification passed"}
except Exception as e:
raise HTTP(500, "Exception in Altcha verification")
@property
def name(self):
return self._name
确保 AltchaServerFixture 可以在实例化 auth 的 common.py 中访问:
from .fixtures import AltchaServerFixture
auth.enable(uses=(session, T, db, AltchaServerFixture()), env=dict(T=T))
这将确保对身份验证表单中的 POST 请求执行 Altcha 验证
你也可以在表单中使用 AltchaServerFixture
@action('other_form', method=['GET', 'POST'])
@action.uses('form_altcha.html', session, flash, AltchaServerFixture())
def other_form():
fields = [
Field("name", requires=IS_NOT_EMPTY()),
Field("color", type="string", requires=IS_IN_SET(["red","blue","green"])),
]
form = Form(fields,
csrf_session=session,
submit_button=T("Submit"))
antes_submit = len(form.structure) - 3
form.structure.insert(antes_submit, XML('<altcha-widget></altcha-widget></br>'))
if form.accepted:
# You can assume here that the Altcha payload was verified by the fixture
flash.set("Form and Altcha successfully verified.")
# Process the form data here
return dict(form=form)